사용자 로그인

이 가이드는 클래스룸 부가기능 둘러보기 시리즈의 두 번째 둘러보기입니다.

이 둘러보기에서는 웹 애플리케이션에 Google 로그인을 추가합니다. 이는 클래스룸 부가기능의 필수 동작입니다. 이후 모든 API 호출에 이 승인 흐름의 사용자 인증 정보를 사용합니다.

이 둘러보기 과정에서는 다음을 완료합니다.

  • iframe 내에서 세션 데이터를 유지하도록 웹 앱을 구성합니다.
  • Google OAuth 2.0 서버 간 로그인 흐름을 구현합니다.
  • OAuth 2.0 API를 호출합니다.
  • API 호출 승인, 로그아웃, 테스트를 지원하는 추가 경로를 만듭니다.

완료되면 웹 앱에서 사용자를 완전히 승인하고 Google API를 호출할 수 있습니다.

승인 흐름 이해

Google API는 인증 및 승인에 OAuth 2.0 프로토콜을 사용합니다. Google OAuth 구현에 대한 자세한 설명은 Google ID OAuth 가이드를 참고하세요.

애플리케이션의 사용자 인증 정보는 Google Cloud에서 관리됩니다. ID를 만든 후 사용자를 승인하고 인증하는 4단계 프로세스를 구현하세요.

  1. 승인을 요청합니다. 이 요청의 일부로 콜백 URL을 제공합니다. 완료되면 승인 URL을 받게 됩니다.
  2. 사용자를 승인 URL로 리디렉션합니다. 결과 페이지에서 앱에 필요한 권한을 사용자에게 알리고 액세스를 허용하라는 메시지를 표시합니다. 완료되면 사용자는 콜백 URL로 라우팅됩니다.
  3. 콜백 경로에서 승인 코드를 받습니다. 액세스 토큰갱신 토큰의 승인 코드를 교환합니다.
  4. 토큰을 사용하여 Google API를 호출합니다.

OAuth 2.0 사용자 인증 정보 가져오기

개요 페이지에 설명된 대로 OAuth 사용자 인증 정보를 만들고 다운로드했는지 확인합니다. 프로젝트에서 사용자 로그인에 이 사용자 인증 정보를 사용해야 합니다.

승인 흐름 구현

웹 앱에 로직과 경로를 추가하여 위에 설명된 흐름을 실현할 수 있도록 다음 기능을 포함합니다.

  • 방문 페이지에 도달하면 승인 과정을 시작합니다.
  • 승인을 요청하고 승인 서버 응답을 처리합니다.
  • 저장된 사용자 인증 정보를 삭제합니다.
  • 앱의 권한을 취소합니다.
  • API 호출을 테스트합니다.

승인 시작

필요한 경우 승인 절차를 시작하도록 방문 페이지를 수정합니다. 부가기능은 현재 세션에 저장된 토큰과 OAuth 2.0 서버에서 토큰을 가져오는 두 가지 상태일 수 있습니다. 세션에 토큰이 있으면 테스트 API 호출을 수행하고, 그렇지 않으면 사용자에게 로그인하라는 메시지를 표시합니다.

Python

routes.py 파일을 엽니다. 먼저 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.")

Java

이 둘러보기의 코드는 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

서비스 클래스 (step_02_sign_in 모듈의 AuthService.java)를 만들어 컨트롤러 파일에서 엔드포인트의 로직을 처리하고 리디렉션 URI, 클라이언트 보안 비밀 파일 위치, 부가기능에 필요한 범위를 설정합니다. 리디렉션 URI는 사용자가 앱을 승인한 후 사용자를 특정 URI로 다시 라우팅하는 데 사용됩니다. client_secret.json 파일을 배치할 위치에 관한 자세한 내용은 소스 코드에 있는 README.md의 프로젝트 설정 섹션을 참고하세요.

@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));
    }
}

컨트롤러 파일 (step_02_sign_in 모듈의 AuthController.java)을 열고 세션에 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에는 요청된 범위, 승인 의 대상 경로, 웹 앱의 클라이언트 ID와 같은 여러 정보가 포함됩니다. 이 샘플 승인 URL에서 확인할 수 있습니다.

Python

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)

flowredirect_uri를 설정합니다. 이는 사용자가 앱을 승인한 후 반환하도록 하려는 경로입니다. 아래 예의 /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_urlstate를 구성합니다. 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)

Java

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 엔드포인트를 추가합니다. 이 엔드포인트는 AuthService authorize() 메서드를 호출하여 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를 수신합니다. 그런 다음 코드를 액세스 토큰과 갱신 토큰으로 교환합니다.

Python

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 인수가 필요합니다. 요청의 전체 URL이므로 authorization_response를 사용합니다.

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")

Java

승인 URL이 수행하는 리디렉션에서 가져온 승인 코드를 전달하여 Credentials 객체를 반환하는 메서드를 서비스 클래스에 추가합니다. 이 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 호출 테스트

이 흐름이 완료되면 이제 Google API를 호출할 수 있습니다.

예를 들어 사용자의 프로필 정보를 요청합니다. OAuth 2.0 API에서 사용자 정보를 요청할 수 있습니다.

Python

OAuth 2.0 검색 API 문서를 읽고 이 API를 사용하여 채워진 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"))

Java

서비스 클래스에서 Credentials를 매개변수로 사용하여 UserInfo 객체를 빌드하는 메서드를 만듭니다.

/** 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);
    }
}

사용자 인증 정보 삭제

현재 세션에서 사용자 인증 정보를 삭제하여 '삭제'할 수 있습니다. 이렇게 하면 부가기능 방문 페이지에서 라우팅을 테스트할 수 있습니다.

부가기능 방문 페이지로 리디렉션하기 전에 사용자가 로그아웃했음을 표시하는 것이 좋습니다. 앱은 승인 절차를 거쳐 새 사용자 인증 정보를 얻어야 하지만 사용자에게 앱을 다시 승인하라는 메시지가 표시되지 않습니다.

Python

@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()를 사용하지만 세션에 다른 값이 저장된 경우 의도하지 않은 영향이 발생할 수 있습니다.

Java

컨트롤러에서 /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에 전송하여 앱의 권한을 취소할 수 있습니다. 요청에는 사용자의 액세스 토큰이 포함되어야 합니다.

Python

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!")

Java

취소 엔드포인트를 호출하는 메서드를 서비스 클래스에 추가합니다.

/** 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 클래스룸에 로그인합니다. 수업 과제 탭으로 이동하여 새 과제를 만듭니다. 텍스트 영역 아래의 부가기능 버튼을 클릭한 다음 부가기능을 선택합니다. iframe이 열리고 부가기능이 GWM SDK의 앱 구성 페이지에서 지정한 첨부파일 설정 URI를 로드합니다.

수고하셨습니다 다음 단계인 부가기능 반복 방문 처리를 진행할 준비가 되었습니다.