Войдите в систему

Это второе пошаговое руководство в серии пошаговых руководств по дополнениям для Класса.

В этом пошаговом руководстве вы добавите вход в Google в веб-приложение. Это обязательное поведение для дополнений Класса. Используйте учетные данные из этого потока авторизации для всех будущих вызовов API.

В ходе этого пошагового руководства вы выполните следующее:

  • Настройте свое веб-приложение для хранения данных сеанса в iframe.
  • Внедрите процесс входа между серверами Google OAuth 2.0.
  • Вызовите API OAuth 2.0.
  • Создайте дополнительные маршруты для поддержки авторизации, выхода и тестирования вызовов API.

После завершения вы можете полностью авторизовать пользователей в своем веб-приложении и отправлять вызовы API Google.

Понимание процесса авторизации

API Google используют протокол OAuth 2.0 для аутентификации и авторизации. Полное описание реализации Google OAuth доступно в руководстве Google Identity OAuth .

Учетные данные вашего приложения управляются в Google Cloud. После их создания реализуйте четырехэтапный процесс аутентификации и авторизации пользователя:

  1. Запросить авторизацию. Укажите URL обратного вызова как часть этого запроса. По завершении вы получите URL-адрес авторизации .
  2. Перенаправьте пользователя на URL-адрес авторизации. Полученная страница информирует пользователя о разрешениях, которые требуются вашему приложению, и предлагает разрешить доступ. По завершении пользователь перенаправляется на URL-адрес обратного вызова.
  3. Получите код авторизации по маршруту обратного вызова. Замените код авторизации на токен доступа и токен обновления .
  4. Выполняйте вызовы API Google, используя токены.

Получите учетные данные OAuth 2.0.

Убедитесь, что вы создали и загрузили учетные данные OAuth , как описано на странице «Обзор» . Ваш проект должен использовать эти учетные данные для входа пользователя.

Реализовать поток авторизации

Добавьте логику и маршруты в наше веб-приложение, чтобы реализовать описанный процесс, включая следующие функции:

  • Запустите процесс авторизации при достижении целевой страницы.
  • Запросить авторизацию и обработать ответ сервера авторизации.
  • Очистите сохраненные учетные данные.
  • Отмените разрешения приложения.
  • Проверьте вызов API.

Инициировать авторизацию

Измените свою целевую страницу, чтобы при необходимости инициировать процесс авторизации. Надстройка может находиться в двух возможных состояниях; либо в текущем сеансе есть сохраненные токены, либо вам необходимо получить токены с сервера OAuth 2.0. Выполните тестовый вызов API, если в сеансе есть токены, или иным образом предложите пользователю войти в систему.

Питон

Откройте файл routes.py . Сначала установите пару констант и нашу конфигурацию файлов cookie в соответствии с рекомендациями по безопасности iframe .

# The file that contains the OAuth 2.0 client_id and client_secret.
CLIENT_SECRETS_FILE = "client_secret.json"

# The OAuth 2.0 access scopes to request.
# These scopes must match the scopes in your Google Cloud project's OAuth Consent
# Screen: https://console.cloud.google.com/apis/credentials/consent
SCOPES = [
    "openid",
    "https://www.googleapis.com/auth/userinfo.profile",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/classroom.addons.teacher",
    "https://www.googleapis.com/auth/classroom.addons.student"
]

# Flask cookie configurations.
app.config.update(
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="None",
)

Перейдите к маршруту приземления вашего дополнения (в файле примера это /classroom-addon ). Добавьте логику для отображения страницы входа, если сеанс не содержит ключ «учетные данные».

@app.route("/classroom-addon")
def classroom_addon():
    if "credentials" not in flask.session:
        return flask.render_template("authorization.html")

    return flask.render_template(
        "addon-discovery.html",
        message="You've reached the addon discovery page.")

Джава

Код для этого пошагового руководства можно найти в модуле step_02_sign_in .

Откройте файл application.properties и добавьте конфигурацию сеанса, соответствующую рекомендациям по безопасности iframe .

# iFrame security recommendations call for cookies to have the HttpOnly and
# secure attribute set
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true

# Ensures that the session is maintained across the iframe and sign-in pop-up.
server.servlet.session.cookie.same-site=none

Создайте класс обслуживания ( AuthService.java в модуле step_02_sign_in ), чтобы обрабатывать логику конечных точек в файле контроллера, и настройте URI перенаправления, расположение файла секретов клиента и области, необходимые для вашего дополнения. URI перенаправления используется для перенаправления ваших пользователей на определенный URI после того, как они авторизуют ваше приложение. См. раздел «Настройка проекта» файла README.md в исходном коде для получения информации о том, где разместить файл client_secret.json .

@Service
public class AuthService {
    private static final String REDIRECT_URI = "https://localhost:5000/callback";
    private static final String CLIENT_SECRET_FILE = "client_secret.json";
    private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
    private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();

    private static final String[] REQUIRED_SCOPES = {
        "https://www.googleapis.com/auth/userinfo.profile",
        "https://www.googleapis.com/auth/userinfo.email",
        "https://www.googleapis.com/auth/classroom.addons.teacher",
        "https://www.googleapis.com/auth/classroom.addons.student"
    };

    /** Creates and returns a Collection object with all requested scopes.
    *   @return Collection of scopes requested by the application.
    */
    public static Collection<String> getScopes() {
        return new ArrayList<>(Arrays.asList(REQUIRED_SCOPES));
    }
}

Откройте файл контроллера ( AuthController.java в модуле step_02_sign_in ) и добавьте логику к целевому маршруту для отображения страницы входа, если сеанс не содержит ключ credentials .

@GetMapping(value = {"/start-auth-flow"})
public String startAuthFlow(Model model) {
    try {
        return "authorization";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

@GetMapping(value = {"/addon-discovery"})
public String addon_discovery(HttpSession session, Model model) {
    try {
        if (session == null || session.getAttribute("credentials") == null) {
            return startAuthFlow(model);
        }
        return "addon-discovery";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

Ваша страница авторизации должна содержать ссылку или кнопку, по которой пользователь может «войти». Нажатие на эту кнопку должно перенаправить пользователя на маршрут authorize .

Запросить авторизацию

Чтобы запросить авторизацию, создайте и перенаправьте пользователя на URL-адрес аутентификации. Этот URL-адрес включает в себя несколько фрагментов информации, таких как запрошенные области, маршрут назначения после авторизации и идентификатор клиента веб-приложения. Вы можете увидеть это в этом образце URL-адреса авторизации .

Питон

Добавьте следующий импорт в файл routes.py .

import google_auth_oauthlib.flow

Создайте новый маршрут /authorize . Создайте экземпляр google_auth_oauthlib.flow.Flow ; для этого мы настоятельно рекомендуем использовать включенный метод from_client_secrets_file .

@app.route("/authorize")
def authorize():
    # Create flow instance to manage the OAuth 2.0 Authorization Grant Flow
    # steps.
    flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
        CLIENT_SECRETS_FILE, scopes=SCOPES)

Установите redirect_uri flow ; это маршрут, по которому вы хотите, чтобы пользователи возвращались после авторизации вашего приложения . В следующем примере это /callback .

# The URI created here must exactly match one of the authorized redirect
# URIs for the OAuth 2.0 client, which you configured in the API Console. If
# this value doesn't match an authorized URI, you will get a
# "redirect_uri_mismatch" error.
flow.redirect_uri = flask.url_for("callback", _external=True)

Используйте объект потока для создания authorization_url и state . Сохраните state в сеансе; позже он используется для проверки подлинности ответа сервера. Наконец, перенаправьте пользователя на authorization_url .

authorization_url, state = flow.authorization_url(
    # Enable offline access so that you can refresh an access token without
    # re-prompting the user for permission. Recommended for web server apps.
    access_type="offline",
    # Enable incremental authorization. Recommended as a best practice.
    include_granted_scopes="true")

# Store the state so the callback can verify the auth server response.
flask.session["state"] = state

# Redirect the user to the OAuth authorization URL.
return flask.redirect(authorization_url)

Джава

Добавьте следующие методы в файл AuthService.java , чтобы создать экземпляр объекта потока, а затем использовать его для получения URL-адреса авторизации:

  • Метод getClientSecrets() считывает секретный файл клиента и создает объект GoogleClientSecrets .
  • Метод getFlow() создает экземпляр GoogleAuthorizationCodeFlow .
  • Метод authorize() использует объект GoogleAuthorizationCodeFlow , параметр state и URI перенаправления для получения URL-адреса авторизации. Параметр state используется для проверки подлинности ответа от сервера авторизации. Затем метод возвращает карту с URL-адресом авторизации и параметром state .
/** Reads the client secret file downloaded from Google Cloud.
 *   @return GoogleClientSecrets read in from client secret file. */
public GoogleClientSecrets getClientSecrets() throws Exception {
    try {
        InputStream in = SignInApplication.class.getClassLoader()
            .getResourceAsStream(CLIENT_SECRET_FILE);
        if (in == null) {
            throw new FileNotFoundException("Client secret file not found: "
                +   CLIENT_SECRET_FILE);
        }
        GoogleClientSecrets clientSecrets = GoogleClientSecrets
            .load(JSON_FACTORY, new InputStreamReader(in));
        return clientSecrets;
    } catch (Exception e) {
        throw e;
    }
}

/** Builds and returns authorization code flow.
*   @return GoogleAuthorizationCodeFlow object used to retrieve an access
*   token and refresh token for the application.
*   @throws Exception if reading client secrets or building code flow object
*   is unsuccessful.
*/
public GoogleAuthorizationCodeFlow getFlow() throws Exception {
    try {
        GoogleAuthorizationCodeFlow authorizationCodeFlow =
            new GoogleAuthorizationCodeFlow.Builder(
                HTTP_TRANSPORT,
                JSON_FACTORY,
                getClientSecrets(),
                getScopes())
                .setAccessType("offline")
                .build();
        return authorizationCodeFlow;
    } catch (Exception e) {
        throw e;
    }
}

/** Builds and returns a map with the authorization URL, which allows the
*   user to give the app permission to their account, and the state parameter,
*   which is used to prevent cross site request forgery.
*   @return map with authorization URL and state parameter.
*   @throws Exception if building the authorization URL is unsuccessful.
*/
public HashMap authorize() throws Exception {
    HashMap<String, String> authDataMap = new HashMap<>();
    try {
        String state = new BigInteger(130, new SecureRandom()).toString(32);
        authDataMap.put("state", state);

        GoogleAuthorizationCodeFlow flow = getFlow();
        String authUrl = flow
            .newAuthorizationUrl()
            .setState(state)
            .setRedirectUri(REDIRECT_URI)
            .build();
        String url = authUrl;
        authDataMap.put("url", url);

        return authDataMap;
    } catch (Exception e) {
        throw e;
    }
}

Используйте внедрение конструктора, чтобы создать экземпляр класса обслуживания в классе контроллера.

/** Declare AuthService to be used in the Controller class constructor. */
private final AuthService authService;

/** AuthController constructor. Uses constructor injection to instantiate
*   the AuthService and UserRepository classes.
*   @param authService the service class that handles the implementation logic
*   of requests.
*/
public AuthController(AuthService authService) {
    this.authService = authService;
}

Добавьте конечную точку /authorize в класс контроллера. Эта конечная точка вызывает метод authorize() AuthService для получения параметра state и URL-адреса авторизации. Затем конечная точка сохраняет параметр state в сеансе и перенаправляет пользователей на URL-адрес авторизации.

/** Redirects the sign-in pop-up to the authorization URL.
*   @param response the current response to pass information to.
*   @param session the current session.
*   @throws Exception if redirection to the authorization URL is unsuccessful.
*/
@GetMapping(value = {"/authorize"})
public void authorize(HttpServletResponse response, HttpSession session)
    throws Exception {
    try {
        HashMap authDataMap = authService.authorize();
        String authUrl = authDataMap.get("url").toString();
        String state = authDataMap.get("state").toString();
        session.setAttribute("state", state);
        response.sendRedirect(authUrl);
    } catch (Exception e) {
        throw e;
    }
}

Обработка ответа сервера

После авторизации пользователь возвращается к маршруту redirect_uri с предыдущего шага. В предыдущем примере это маршрут /callback .

Вы получаете code в ответе, когда пользователь возвращается со страницы авторизации. Затем замените код на токены доступа и обновления:

Питон

Добавьте следующий импорт в файл сервера Flask.

import google.oauth2.credentials
import googleapiclient.discovery

Добавьте маршрут на свой сервер. Создайте еще один экземпляр google_auth_oauthlib.flow.Flow , но на этот раз повторно используйте состояние, сохраненное на предыдущем шаге.

@app.route("/callback")
def callback():
    state = flask.session["state"]

    flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
        CLIENT_SECRETS_FILE, scopes=SCOPES, state=state)
    flow.redirect_uri = flask.url_for("callback", _external=True)

Затем запросите доступ и обновите токены. К счастью, объект flow также содержит метод fetch_token для достижения этой цели. Метод ожидает аргументы code authorization_response . Используйте authorization_response , так как это полный URL-адрес запроса.

authorization_response = flask.request.url
flow.fetch_token(authorization_response=authorization_response)

Теперь у вас есть полные полномочия! Сохраните их в сеансе, чтобы их можно было получить другими методами или маршрутами, а затем перенаправьте на целевую страницу надстройки.

credentials = flow.credentials
flask.session["credentials"] = {
    "token": credentials.token,
    "refresh_token": credentials.refresh_token,
    "token_uri": credentials.token_uri,
    "client_id": credentials.client_id,
    "client_secret": credentials.client_secret,
    "scopes": credentials.scopes
}

# Close the pop-up by rendering an HTML page with a script that redirects
# the owner and closes itself. This can be done with a bit of JavaScript:
# <script>
#     window.opener.location.href = "{{ url_for('classroom_addon') }}";
#     window.close();
# </script>
return flask.render_template("close-me.html")

Джава

Добавьте в свой класс службы метод, который возвращает объект Credentials , передавая код авторизации, полученный в результате перенаправления, выполняемого URL-адресом авторизации. Этот объект Credentials используется позже для получения токена доступа и токена обновления.

/** Returns the required credentials to access Google APIs.
*   @param authorizationCode the authorization code provided by the
*   authorization URL that's used to obtain credentials.
*   @return the credentials that were retrieved from the authorization flow.
*   @throws Exception if retrieving credentials is unsuccessful.
*/
public Credential getAndSaveCredentials(String authorizationCode) throws Exception {
    try {
        GoogleAuthorizationCodeFlow flow = getFlow();
        GoogleClientSecrets googleClientSecrets = getClientSecrets();
        TokenResponse tokenResponse = flow.newTokenRequest(authorizationCode)
            .setClientAuthentication(new ClientParametersAuthentication(
                googleClientSecrets.getWeb().getClientId(),
                googleClientSecrets.getWeb().getClientSecret()))
            .setRedirectUri(REDIRECT_URI)
            .execute();
        Credential credential = flow.createAndStoreCredential(tokenResponse, null);
        return credential;
    } catch (Exception e) {
        throw e;
    }
}

Добавьте конечную точку для URI перенаправления на контроллер. Получите код авторизации и параметр state из запроса. Сравните этот параметр state с атрибутом state , хранящимся в сеансе. Если они совпадают, продолжайте процесс авторизации. Если они не совпадают, верните ошибку.

Затем вызовите метод AuthService getAndSaveCredentials и передайте код авторизации в качестве параметра. После получения объекта Credentials сохраните его в сеансе. Затем закройте диалоговое окно и перенаправьте пользователя на целевую страницу дополнения.

/** Handles the redirect URL to grant the application access to the user's
*   account.
*   @param request the current request used to obtain the authorization code
*   and state parameter from.
*   @param session the current session.
*   @param response the current response to pass information to.
*   @param model the Model interface to pass error information that's
*   displayed on the error page.
*   @return the close-pop-up template if authorization is successful, or the
*   onError method to handle and display the error message.
*/
@GetMapping(value = {"/callback"})
public String callback(HttpServletRequest request, HttpSession session,
    HttpServletResponse response, Model model) {
    try {
        String authCode = request.getParameter("code");
        String requestState = request.getParameter("state");
        String sessionState = session.getAttribute("state").toString();
        if (!requestState.equals(sessionState)) {
            response.setStatus(401);
            return onError("Invalid state parameter.", model);
        }
        Credential credentials = authService.getAndSaveCredentials(authCode);
        session.setAttribute("credentials", credentials);
        return "close-pop-up";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

Проверка вызова API

После завершения потока вы можете отправлять вызовы API Google!

Например, запросите информацию о профиле пользователя. Вы можете запросить информацию о пользователе у API OAuth 2.0.

Питон

Прочтите документацию по API обнаружения OAuth 2.0. Используйте ее, чтобы получить заполненный объект UserInfo .

# Retrieve the credentials from the session data and construct a
# Credentials instance.
credentials = google.oauth2.credentials.Credentials(
    **flask.session["credentials"])

# Construct the OAuth 2.0 v2 discovery API library.
user_info_service = googleapiclient.discovery.build(
    serviceName="oauth2", version="v2", credentials=credentials)

# Request and store the username in the session.
# This allows it to be used in other methods or in an HTML template.
flask.session["username"] = (
    user_info_service.userinfo().get().execute().get("name"))

Джава

Создайте в классе службы метод, который создает объект UserInfo , используя Credentials в качестве параметра.

/** Obtains the Userinfo object by passing in the required credentials.
*   @param credentials retrieved from the authorization flow.
*   @return the Userinfo object for the currently signed-in user.
*   @throws IOException if creating UserInfo service or obtaining the
*   Userinfo object is unsuccessful.
*/
public Userinfo getUserInfo(Credential credentials) throws IOException {
    try {
        Oauth2 userInfoService = new Oauth2.Builder(
            new NetHttpTransport(),
            new GsonFactory(),
            credentials).build();
        Userinfo userinfo = userInfoService.userinfo().get().execute();
        return userinfo;
    } catch (Exception e) {
        throw e;
    }
}

Добавьте конечную точку /test к контроллеру, который отображает электронную почту пользователя.

/** Returns the test request page with the user's email.
*   @param session the current session.
*   @param model the Model interface to pass error information that's
*   displayed on the error page.
*   @return the test page that displays the current user's email or the
*   onError method to handle and display the error message.
*/
@GetMapping(value = {"/test"})
public String test(HttpSession session, Model model) {
    try {
        Credential credentials = (Credential) session.getAttribute("credentials");
        Userinfo userInfo = authService.getUserInfo(credentials);
        String userInfoEmail = userInfo.getEmail();
        if (userInfoEmail != null) {
            model.addAttribute("userEmail", userInfoEmail);
        } else {
            return onError("Could not get user email.", model);
        }
        return "test";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

Очистить учетные данные

Вы можете «очистить» учетные данные пользователя, удалив их из текущего сеанса. Это позволит вам протестировать маршрутизацию на целевой странице дополнения.

Мы рекомендуем показывать признак того, что пользователь вышел из системы, прежде чем перенаправлять его на целевую страницу дополнения. Ваше приложение должно пройти процесс авторизации для получения новых учетных данных, но пользователям не будет предложено повторно авторизовать ваше приложение.

Питон

@app.route("/clear")
def clear_credentials():
    if "credentials" in flask.session:
        del flask.session["credentials"]
        del flask.session["username"]

    return flask.render_template("signed-out.html")

В качестве альтернативы используйте flask.session.clear() , но это может иметь непредвиденные последствия, если в сеансе хранятся другие значения.

Джава

В контроллере добавьте конечную точку /clear .

/** Clears the credentials in the session and returns the sign-out
*   confirmation page.
*   @param session the current session.
*   @return the sign-out confirmation page.
*/
@GetMapping(value = {"/clear"})
public String clear(HttpSession session) {
    try {
        if (session != null && session.getAttribute("credentials") != null) {
            session.removeAttribute("credentials");
        }
        return "sign-out";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

Отозвать разрешение приложения

Пользователь может отозвать разрешение вашего приложения, отправив запрос POST на адрес https://oauth2.googleapis.com/revoke . Запрос должен содержать токен доступа пользователя.

Питон

import requests

@app.route("/revoke")
def revoke():
    if "credentials" not in flask.session:
        return flask.render_template("addon-discovery.html",
                            message="You need to authorize before " +
                            "attempting to revoke credentials.")

    credentials = google.oauth2.credentials.Credentials(
        **flask.session["credentials"])

    revoke = requests.post(
        "https://oauth2.googleapis.com/revoke",
        params={"token": credentials.token},
        headers={"content-type": "application/x-www-form-urlencoded"})

    if "credentials" in flask.session:
        del flask.session["credentials"]
        del flask.session["username"]

    status_code = getattr(revoke, "status_code")
    if status_code == 200:
        return flask.render_template("authorization.html")
    else:
        return flask.render_template(
            "index.html", message="An error occurred during revocation!")

Джава

Добавьте в класс службы метод, который вызывает конечную точку отзыва.

/** Revokes the app's permissions to the user's account.
*   @param credentials retrieved from the authorization flow.
*   @return response entity returned from the HTTP call to obtain response
*   information.
*   @throws RestClientException if the POST request to the revoke endpoint is
*   unsuccessful.
*/
public ResponseEntity<String> revokeCredentials(Credential credentials) throws RestClientException {
    try {
        String accessToken = credentials.getAccessToken();
        String url = "https://oauth2.googleapis.com/revoke?token=" + accessToken;

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
        HttpEntity<Object> httpEntity = new HttpEntity<Object>(httpHeaders);
        ResponseEntity<String> responseEntity = new RestTemplate().exchange(
            url,
            HttpMethod.POST,
            httpEntity,
            String.class);
        return responseEntity;
    } catch (RestClientException e) {
        throw e;
    }
}

Добавьте конечную точку /revoke к контроллеру, который очищает сеанс и перенаправляет пользователя на страницу авторизации, если отзыв прошел успешно.

/** Revokes the app's permissions and returns the authorization page.
*   @param session the current session.
*   @return the authorization page.
*   @throws Exception if revoking access is unsuccessful.
*/
@GetMapping(value = {"/revoke"})
public String revoke(HttpSession session) throws Exception {
    try {
        if (session != null && session.getAttribute("credentials") != null) {
            Credential credentials = (Credential) session.getAttribute("credentials");
            ResponseEntity responseEntity = authService.revokeCredentials(credentials);
            Integer httpStatusCode = responseEntity.getStatusCodeValue();

            if (httpStatusCode != 200) {
                return onError("There was an issue revoking access: " +
                    responseEntity.getStatusCode(), model);
            }
            session.removeAttribute("credentials");
        }
        return startAuthFlow(model);
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

Протестируйте дополнение

Войдите в Google Classroom как один из тестовых пользователей вашего учителя . Перейдите на вкладку «Задания» и создайте новое задание . Нажмите кнопку «Дополнения» под текстовой областью, затем выберите дополнение. Откроется iframe, и надстройка загрузит URI настройки вложения , который вы указали на странице конфигурации приложения GWM SDK.

Поздравляем! Вы готовы перейти к следующему шагу: обработке повторных посещений вашего дополнения .