사용자 로그인

클래스룸 부가기능의 두 번째 둘러보기입니다. 오신 것을 환영합니다

이 둘러보기에서는 웹 애플리케이션에 Google 로그인을 추가합니다. 이것은 필수 동작입니다. 다음에서 사용자 인증 정보를 사용합니다. 이 승인 흐름이 있어야 합니다.

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

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

완료되면 웹 앱에서 사용자를 완전히 승인하고 Google API

승인 흐름 이해

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

애플리케이션의 사용자 인증 정보는 Google Cloud에서 관리됩니다. 이러한 작업이 완료되면 4단계 프로세스를 구현하여 AI를 인증하고 사용자:

  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)로 이동합니다. 파일 참조). 세션에 다음이 포함되지 않은 경우 로그인 페이지를 렌더링하는 로직 추가 'credentials' 키를 누릅니다.

@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

서비스 클래스 만들기 (step_02_sign_in 모듈의 AuthService.java) 컨트롤러 파일의 엔드포인트 이면에 있는 로직을 처리하고 부가기능의 리디렉션 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));
    }
}

컨트롤러 파일 (step_02_sign_inAuthController.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);
    }
}

승인 페이지에는 사용자가 서명할 수 있는 링크 또는 버튼이 있어야 합니다. in'). 이 링크를 클릭하면 사용자가 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 이 URL은 문서의 진위 여부를 나중에 서버 응답을 반환합니다 마지막으로 사용자를 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 매개변수와 승인 URL을 검색하기 위한 리디렉션 URI가 포함되어 있습니다. 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 인수 사용 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 호출 테스트

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

예를 들어 사용자의 프로필 정보를 요청합니다. 이 사용자 정보를 가져올 수 있습니다.

Python

자세한 내용은 OAuth 2.0 Discovery 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"))

자바

서비스 클래스에서 다음을 사용하여 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);
    }
}

자격증명 삭제

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

사용자가 이전에 로그아웃한 적이 있음을 표시하는 것이 좋습니다. 부가기능 방문 페이지로 리디렉션합니다. 앱은 인증 흐름을 통해 새 사용자 인증 정보를 가져올 수 있지만 사용자에게 앱을 다시 인증해야 합니다.

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

자바

컨트롤러에서 /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!")

자바

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

/** 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 구성 있습니다.

축하합니다. 다음 단계인 반복 처리로 넘어갈 준비가 되었습니다. 확인할 수 있습니다.