반복 로그인 처리

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

이 둘러보기에서는 이전에 부여된 사용자 인증 정보를 자동으로 가져와 부가기능 재방문 시 이를 처리합니다. 그런 다음 API 요청을 즉시 실행할 수 있는 페이지로 사용자를 라우팅합니다. 이는 클래스룸 부가기능의 필수 동작입니다.

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

  • 사용자 인증 정보를 위한 영구 저장소를 구현합니다.
  • 다음 부가기능 쿼리 매개변수를 검색하고 평가합니다.
    • login_hint: 로그인한 사용자의 Google ID 번호입니다.
    • hd: 로그인한 사용자의 도메인입니다.

이 중 하나만 전송됩니다. 사용자가 아직 앱을 승인하지 않은 경우 Classroom API는 hd 매개변수를 전송합니다. 승인되지 않은 경우 login_hint를 전송합니다. 쿼리 매개변수의 전체 목록은 iframe 가이드 페이지에서 확인할 수 있습니다.

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

iframe 쿼리 매개변수 이해

클래스룸을 열면 부가기능의 연결 설정 URI가 로드됩니다. 클래스룸에서는 URI에 여러 GET 쿼리 매개변수를 추가합니다. 이러한 매개변수에는 유용한 문맥 정보가 포함됩니다. 예를 들어 첨부파일 검색 URI가 https://example.com/addon이면 클래스룸은 소스 URL이 https://example.com/addon?courseId=XXX&postId=YYY&addOnToken=ZZZ로 설정된 iframe을 만듭니다. 여기서 XXX, YYY, ZZZ는 문자열 ID입니다. 이 시나리오에 관한 자세한 설명은 iframe 가이드를 참고하세요.

탐색 URL에 사용할 수 있는 쿼리 매개변수는 5가지입니다.

  • courseId: 현재 클래스룸 과정의 ID입니다.
  • postId: 사용자가 수정하거나 만드는 할당 게시물의 ID입니다.
  • addOnToken: 특정 클래스룸 부가기능 작업을 승인하는 데 사용되는 토큰입니다.
  • login_hint: 현재 사용자의 Google ID입니다.
  • hd: 현재 사용자의 호스트 도메인(예: example.com)

이 둘러보기에서는 hdlogin_hint를 다룹니다. 제공된 쿼리 매개변수에 따라 사용자는 승인 흐름(hd인 경우) 또는 부가기능 검색 페이지(login_hint인 경우)로 라우팅됩니다.

쿼리 매개변수에 액세스하기

위에서 설명한 대로 쿼리 매개변수는 URI 문자열로 웹 애플리케이션에 전달됩니다. 이러한 값을 세션에 저장하세요. 이 값은 승인 흐름에 사용되고 사용자에 관한 정보를 저장 및 검색하는 데 사용됩니다. 이러한 쿼리 매개변수는 부가기능을 처음 열 때만 전달됩니다.

Python

Flask 경로의 정의로 이동합니다 (제공된 예시를 따르는 경우 routes.py). 부가기능 방문 경로(제공된 예에서는 /classroom-addon) 상단에서 login_hinthd 쿼리 매개변수를 검색하고 저장합니다.

# Retrieve the login_hint and hd query parameters.
login_hint = flask.request.args.get("login_hint")
hd = flask.request.args.get("hd")

login_hinthd가 세션에 저장되어 있는지 확인합니다. 이는 이러한 값을 저장하기에 적절한 위치입니다. 임시 값이며 부가기능이 열리면 새 값을 받게 됩니다.

# It's possible that we might return to this route later, in which case the
# parameters will not be passed in. Instead, use the values cached in the
# session.

# If neither query parameter is available, use the values in the session.
if login_hint is None and hd is None:
    login_hint = flask.session.get("login_hint")
    hd = flask.session.get("hd")

# If there's no login_hint query parameter, then check for hd.
# Send the user to the sign in page.
elif hd is not None:
    flask.session["hd"] = hd
    return start_auth_flow()

# If the login_hint query parameter is available, we'll store it in the
# session.
else:
    flask.session["login_hint"] = login_hint

Java

컨트롤러 클래스의 부가기능 랜딩 경로(제공된 예에서는 AuthController.java/addon-discovery)로 이동합니다. 이 경로의 시작 부분에서 login_hinthd 쿼리 매개변수를 검색하고 저장합니다.

/** Retrieve the login_hint or hd query parameters from the request URL. */
String login_hint = request.getParameter("login_hint");
String hd = request.getParameter("hd");

login_hinthd가 세션에 저장되어 있는지 확인합니다. 이는 이러한 값을 저장하기에 적절한 위치입니다. 임시 값이며 부가기능이 열리면 새 값을 받게 됩니다.

/** If neither query parameter is sent, use the values in the session. */
if (login_hint == null && hd == null) {
    login_hint = (String) session.getAttribute("login_hint");
    hd = (String) session.getAttribute("hd");
}

/** If the hd query parameter is provided, add hd to the session and route
*   the user to the authorization page. */
else if (hd != null) {
    session.setAttribute("hd", hd);
    return startAuthFlow(model);
}

/** If the login_hint query parameter is provided, add it to the session. */
else if (login_hint != null) {
    session.setAttribute("login_hint", login_hint);
}

승인 흐름에 쿼리 매개변수 추가

login_hinthd 매개변수도 Google의 인증 서버에 전달되어야 합니다. 이렇게 하면 인증 프로세스가 용이해집니다. 애플리케이션이 인증을 시도하는 사용자를 알고 있는 경우 서버는 힌트를 사용하여 로그인 양식의 이메일 필드를 미리 채우는 방식으로 로그인 흐름을 간소화합니다.

Python

Flask 서버 파일 (제공된 예시에서는 /authorize)에서 승인 경로로 이동합니다. flow.authorization_url 호출에 login_hinthd 인수를 추가합니다.

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",
    # The user will automatically be selected if we have the login_hint.
    login_hint=flask.session.get("login_hint"),
    # If we don't have login_hint, passing hd will reduce the list of
    # accounts in the account chooser to only those with the same domain.
    hd=flask.session.get("hd"))

Java

AuthService.java 클래스의 authorize() 메서드로 이동합니다. login_hinthd를 메서드에 매개변수로 추가하고 login_hinthd 인수를 승인 URL 빌더에 추가합니다.

String authUrl = flow
    .newAuthorizationUrl()
    .setState(state)
    .set("login_hint", login_hint)
    .set("hd", hd)
    .setRedirectUri(REDIRECT_URI)
    .build();

사용자 인증 정보를 위한 영구 스토리지 추가

부가기능이 로드될 때 login_hint를 쿼리 매개변수로 수신하면 이는 사용자가 이미 애플리케이션의 승인 과정을 완료했음을 나타냅니다. 강제로 다시 로그인하도록 하지 말고 이전 사용자 인증 정보를 검색해야 합니다.

승인 흐름이 완료되면 갱신 토큰을 받았음을 기억하세요. 이 토큰을 저장하여 액세스 토큰을 얻는 데 재사용하세요. 액세스 토큰은 단기간만 가능하며 Google API를 사용하는 데 필요합니다. 이전에 이러한 사용자 인증 정보를 세션에 저장했지만 반복 방문을 처리하려면 이 사용자 인증 정보를 저장해야 합니다.

사용자 스키마 정의 및 데이터베이스 설정

User의 데이터베이스 스키마를 설정합니다.

Python

사용자 스키마 정의

User에는 다음 속성이 포함됩니다.

  • id: 사용자의 Google ID입니다. 이 값은 login_hint 쿼리 매개변수에 제공된 값과 일치해야 합니다.
  • display_name: 사용자의 성과 이름(예: 'Alex Smith')입니다.
  • email: 사용자의 이메일 주소입니다.
  • portrait_url: 사용자 프로필 사진의 URL입니다.
  • refresh_token: 이전에 획득한 갱신 토큰입니다.

이 예에서는 Python에서 기본적으로 지원되는 SQLite를 사용하여 저장소를 구현합니다. flask_sqlalchemy 모듈을 사용하여 데이터베이스 관리를 용이하게 합니다.

데이터베이스 설정

먼저 데이터베이스의 파일 위치를 지정합니다. 서버 구성 파일 (제공된 예의 config.py)로 이동하여 다음을 추가합니다.

import os

# Point to a database file in the project root.
DATABASE_FILE_NAME = os.path.join(
    os.path.abspath(os.path.dirname(__file__)), 'data.sqlite')

class Config(object):
    SQLALCHEMY_DATABASE_URI = f"sqlite:///{DATABASE_FILE_NAME}"
    SQLALCHEMY_TRACK_MODIFICATIONS = False

그러면 Flask가 main.py 파일과 동일한 디렉터리에 있는 data.sqlite 파일을 가리킵니다.

다음으로 모듈 디렉터리로 이동하여 새 models.py 파일을 만듭니다. 제공된 예를 따르는 경우 webapp/models.py입니다. 새 파일에 다음을 추가하여 User 테이블을 정의하고, 다른 경우 모듈 이름을 webapp로 바꿉니다.

from webapp import db

# Database model to represent a user.
class User(db.Model):
    # The user's identifying information:
    id = db.Column(db.String(120), primary_key=True)
    display_name = db.Column(db.String(80))
    email = db.Column(db.String(120), unique=True)
    portrait_url = db.Column(db.Text())

    # The user's refresh token, which will be used to obtain an access token.
    # Note that refresh tokens will become invalid if:
    # - The refresh token has not been used for six months.
    # - The user revokes your app's access permissions.
    # - The user changes passwords.
    # - The user belongs to a Google Cloud organization
    #   that has session control policies in effect.
    refresh_token = db.Column(db.Text())

마지막으로 모듈의 __init__.py 파일에서 다음을 추가하여 새 모델을 가져오고 데이터베이스를 만듭니다.

from webapp import models
from os import path
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy(app)

# Initialize the database file if not created.
if not path.exists(config.DATABASE_FILE_NAME):
    db.create_all()

Java

사용자 스키마 정의

User에는 다음 속성이 포함됩니다.

  • id: 사용자의 Google ID입니다. 이 값은 login_hint 쿼리 매개변수에 제공된 값과 일치해야 합니다.
  • email: 사용자의 이메일 주소입니다.

모듈의 resources 디렉터리에 schema.sql 파일을 만듭니다. Spring은 이 파일을 읽고 그에 따라 데이터베이스의 스키마를 생성합니다. 테이블 이름, users, User 속성(id, email)을 나타내는 열로 테이블을 정의합니다.

CREATE TABLE IF NOT EXISTS users (
    id VARCHAR(255) PRIMARY KEY, -- user's unique Google ID
    email VARCHAR(255), -- user's email address
);

Java 클래스를 만들어 데이터베이스의 User 모델을 정의합니다. 제공된 예에서 User.java입니다.

@Entity 주석을 추가하여 데이터베이스에 저장할 수 있는 POJO임을 나타냅니다. schema.sql에서 구성한 해당 테이블 이름과 함께 @Table 주석을 추가합니다.

코드 예에는 두 속성의 생성자와 setter가 포함되어 있습니다. 생성자와 setter는 AuthController.java에서 데이터베이스에서 사용자를 만들거나 업데이트하는 데 사용됩니다. 필요에 따라 getter와 toString 메서드를 포함할 수도 있지만 이 특정 둘러보기에서는 이러한 메서드가 사용되지 않으며 간결성을 위해 이 페이지의 코드 예에서는 생략합니다.

/** An entity class that provides a model to store user information. */
@Entity
@Table(name = "users")
public class User {
    /** The user's unique Google ID. The @Id annotation specifies that this
     *   is the primary key. */
    @Id
    @Column
    private String id;

    /** The user's email address. */
    @Column
    private String email;

    /** Required User class no args constructor. */
    public User() {
    }

    /** The User class constructor that creates a User object with the
    *   specified parameters.
    *   @param id the user's unique Google ID
    *   @param email the user's email address
    */
    public User(String id, String email) {
        this.id = id;
        this.email = email;
    }

    public void setId(String id) { this.id = id; }

    public void setEmail(String email) { this.email = email; }
}

UserRepository.java라는 인터페이스를 만들어 데이터베이스에 대한 CRUD 작업을 처리합니다. 이 인터페이스는 CrudRepository 인터페이스를 확장합니다.

/** Provides CRUD operations for the User class by extending the
 *   CrudRepository interface. */
@Repository
public interface UserRepository extends CrudRepository<User, String> {
}

컨트롤러 클래스는 클라이언트와 저장소 간의 통신을 용이하게 합니다. 따라서 UserRepository 클래스를 삽입하도록 컨트롤러 클래스 생성자를 업데이트합니다.

/** Declare UserRepository to be used in the Controller class constructor. */
private final UserRepository userRepository;

/**
*   ...
*   @param userRepository the class that interacts with User objects stored in
*   persistent storage.
*/
public AuthController(AuthService authService, UserRepository userRepository) {
    this.authService = authService;
    this.userRepository = userRepository;
}

데이터베이스 설정

사용자 관련 정보를 저장하려면 Spring Boot에서 기본적으로 지원되는 H2 데이터베이스를 사용합니다. 이 데이터베이스는 후속 둘러보기에서도 클래스룸 관련 다른 정보를 저장하는 데 사용됩니다. H2 데이터베이스를 설정하려면 application.properties에 다음 구성을 추가해야 합니다.

# Enable configuration for persistent storage using an H2 database
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:file:./h2/userdb
spring.datasource.username=<USERNAME>
spring.datasource.password=<PASSWORD>
spring.jpa.hibernate.ddl-auto=update
spring.jpa.open-in-view=false

spring.datasource.url 구성은 userdb 파일이 저장된 h2라는 디렉터리를 만듭니다. H2 데이터베이스 경로를 .gitignore에 추가합니다. 원하는 사용자 이름과 비밀번호로 데이터베이스를 설정하기 위해 애플리케이션을 실행하기 전에 spring.datasource.usernamespring.datasource.password를 업데이트해야 합니다. 애플리케이션을 실행한 후 데이터베이스의 사용자 이름과 비밀번호를 업데이트하려면 생성된 h2 디렉터리를 삭제하고 구성을 업데이트한 후 애플리케이션을 다시 실행합니다.

spring.jpa.hibernate.ddl-auto 구성을 update로 설정하면 애플리케이션이 다시 시작될 때 데이터베이스에 저장된 데이터가 보존됩니다. 애플리케이션이 다시 시작될 때마다 데이터베이스를 지우려면 이 구성을 create로 설정합니다.

spring.jpa.open-in-view 구성을 false로 설정합니다. 이 구성은 기본적으로 사용 설정되어 있으며 프로덕션에서 진단하기 어려운 성능 문제를 일으키는 것으로 알려져 있습니다.

앞에서 설명한 것처럼 반복 사용자의 사용자 인증 정보를 검색할 수 있어야 합니다. 이는 GoogleAuthorizationCodeFlow에서 제공하는 기본 제공 사용자 인증 정보 저장소 지원을 통해 이루어집니다.

AuthService.java 클래스에서 사용자 인증 정보 클래스가 저장된 파일의 경로를 정의합니다. 이 예시에서 파일은 /credentialStore 디렉터리에 생성됩니다. 사용자 인증 정보 저장소 경로를 .gitignore에 추가합니다. 이 디렉터리는 사용자가 승인 흐름을 시작하면 생성됩니다.

private static final File dataDirectory = new File("credentialStore");

그런 다음 AuthService.java 파일에 FileDataStoreFactory 객체를 만들고 반환하는 메서드를 만듭니다. 사용자 인증 정보를 저장하는 Datastore입니다.

/** Creates and returns FileDataStoreFactory object to store credentials.
 *   @return FileDataStoreFactory dataStore used to save and obtain users ids
 *   mapped to Credentials.
 *   @throws IOException if creating the dataStore is unsuccessful.
 */
public FileDataStoreFactory getCredentialDataStore() throws IOException {
    FileDataStoreFactory dataStore = new FileDataStoreFactory(dataDirectory);
    return dataStore;
}

AuthService.javagetFlow() 메서드를 업데이트하여 GoogleAuthorizationCodeFlow Builder() 메서드에 setDataStoreFactory를 포함하고 getCredentialDataStore()를 호출하여 Datastore를 설정합니다.

GoogleAuthorizationCodeFlow authorizationCodeFlow =
    new GoogleAuthorizationCodeFlow.Builder(
        HTTP_TRANSPORT,
        JSON_FACTORY,
        getClientSecrets(),
        getScopes())
    .setAccessType("offline")
    .setDataStoreFactory(getCredentialDataStore())
    .build();

그런 다음 getAndSaveCredentials(String authorizationCode) 메서드를 업데이트합니다. 이전에 이 메서드는 어디에도 저장하지 않고 사용자 인증 정보를 가져왔습니다. 사용자 ID로 색인이 생성된 데이터 스토어에 사용자 인증 정보를 저장하도록 메서드를 업데이트합니다.

id_token를 사용하여 TokenResponse 객체에서 사용자 ID를 가져올 수 있지만 먼저 확인해야 합니다. 그러지 않으면 클라이언트 애플리케이션에서 수정된 사용자 ID를 서버에 전송하여 사용자를 가장할 수 있습니다. Google API 클라이언트 라이브러리를 사용하여 id_token의 유효성을 검사하는 것이 좋습니다. 자세한 내용은 [Google ID 토큰 인증에 관한 Google ID 페이지] 를 참조하세요.

// Obtaining the id_token will help determine which user signed in to the application.
String idTokenString = tokenResponse.get("id_token").toString();

// Validate the id_token using the GoogleIdTokenVerifier object.
GoogleIdTokenVerifier googleIdTokenVerifier = new GoogleIdTokenVerifier.Builder(
        HTTP_TRANSPORT,
        JSON_FACTORY)
    .setAudience(Collections.singletonList(
        googleClientSecrets.getWeb().getClientId()))
    .build();

GoogleIdToken idToken = googleIdTokenVerifier.verify(idTokenString);

if (idToken == null) {
    throw new Exception("Invalid ID token.");
}

id_token가 확인되면 가져온 사용자 인증 정보와 함께 저장할 userId를 가져옵니다.

// Obtain the user id from the id_token.
Payload payload = idToken.getPayload();
String userId = payload.getSubject();

userId를 포함하도록 flow.createAndStoreCredential 호출을 업데이트합니다.

// Save the user id and credentials to the configured FileDataStoreFactory.
Credential credential = flow.createAndStoreCredential(tokenResponse, userId);

Datastore에 특정 사용자의 사용자 인증 정보가 있는 경우 해당 사용자의 사용자 인증 정보를 반환하는 메서드를 AuthService.java 클래스에 추가합니다.

/** Find credentials in the datastore based on a specific user id.
*   @param userId key to find in the file datastore.
*   @return Credential object to be returned if a matching key is found in the datastore. Null if
*   the key doesn't exist.
*   @throws Exception if building flow object or checking for userId key is unsuccessful. */
public Credential loadFromCredentialDataStore(String userId) throws Exception {
    try {
        GoogleAuthorizationCodeFlow flow = getFlow();
        Credential credential = flow.loadCredential(userId);
        return credential;
    } catch (Exception e) {
        e.printStackTrace();
        throw e;
    }
}

사용자 인증 정보 검색

Users를 가져오는 메서드를 정의합니다. login_hint 쿼리 매개변수에 특정 사용자 레코드를 검색하는 데 사용할 수 있는 id가 제공됩니다.

Python

def get_credentials_from_storage(id):
    """
    Retrieves credentials from the storage and returns them as a dictionary.
    """
    return User.query.get(id)

Java

AuthController.java 클래스에서 사용자 ID에 따라 데이터베이스에서 사용자를 검색하는 메서드를 정의합니다.

/** Retrieves stored credentials based on the user id.
*   @param id the id of the current user
*   @return User the database entry corresponding to the current user or null
*   if the user doesn't exist in the database.
*/
public User getUser(String id) {
    if (id != null) {
        Optional<User> user = userRepository.findById(id);
        if (user.isPresent()) {
            return user.get();
        }
    }
    return null;
}

사용자 인증 정보 저장

사용자 인증 정보를 저장하는 데는 두 가지 시나리오가 있습니다. 사용자의 id가 이미 데이터베이스에 있는 경우 기존 레코드를 새 값으로 업데이트합니다. 그렇지 않으면 새 User 레코드를 만들어 데이터베이스에 추가합니다.

Python

먼저 저장 또는 업데이트 동작을 구현하는 유틸리티 메서드를 정의합니다.

def save_user_credentials(credentials=None, user_info=None):
    """
    Updates or adds a User to the database. A new user is added only if both
    credentials and user_info are provided.

    Args:
        credentials: An optional Credentials object.
        user_info: An optional dict containing user info returned by the
            OAuth 2.0 API.
    """

    existing_user = get_credentials_from_storage(
        flask.session.get("login_hint"))

    if existing_user:
        if user_info:
            existing_user.id = user_info.get("id")
            existing_user.display_name = user_info.get("name")
            existing_user.email = user_info.get("email")
            existing_user.portrait_url = user_info.get("picture")

        if credentials and credentials.refresh_token is not None:
            existing_user.refresh_token = credentials.refresh_token

    elif credentials and user_info:
        new_user = User(id=user_info.get("id"),
                        display_name=user_info.get("name"),
                        email=user_info.get("email"),
                        portrait_url=user_info.get("picture"),
                        refresh_token=credentials.refresh_token)

        db.session.add(new_user)

    db.session.commit()

사용자 인증 정보를 데이터베이스에 저장할 수 있는 경우는 두 가지로, 사용자가 승인 과정이 끝날 때 애플리케이션으로 돌아올 때와 API 호출을 실행하는 경우입니다. 여기에서 이전에 세션 credentials 키를 설정했습니다.

callback 경로 끝에서 save_user_credentials을 호출합니다. 사용자 이름만 추출하는 대신 user_info 객체를 유지합니다.

# The flow is complete! We'll use the credentials to fetch the user's info.
user_info_service = googleapiclient.discovery.build(
    serviceName="oauth2", version="v2", credentials=credentials)

user_info = user_info_service.userinfo().get().execute()

flask.session["username"] = user_info.get("name")

save_user_credentials(credentials, user_info)

또한 API 호출 후에 사용자 인증 정보를 업데이트해야 합니다. 이 경우 업데이트된 사용자 인증 정보를 save_user_credentials 메서드에 인수로 제공하면 됩니다.

# Save credentials in case access token was refreshed.
flask.session["credentials"] = credentials_to_dict(credentials)
save_user_credentials(credentials)

Java

먼저 H2 데이터베이스에 User 객체를 저장하거나 업데이트하는 메서드를 정의합니다.

/** Adds or updates a user in the database.
*   @param credential the credentials object to save or update in the database.
*   @param userinfo the userinfo object to save or update in the database.
*   @param session the current session.
*/
public void saveUser(Credential credential, Userinfo userinfo, HttpSession session) {
    User storedUser = null;
    if (session != null && session.getAttribute("login_hint") != null) {
        storedUser = getUser(session.getAttribute("login_hint").toString());
    }

    if (storedUser != null) {
        if (userinfo != null) {
            storedUser.setId(userinfo.getId());
            storedUser.setEmail(userinfo.getEmail());
        }
        userRepository.save(storedUser);
    } else if (credential != null && userinfo != null) {
        User newUser = new User(
            userinfo.getId(),
            userinfo.getEmail(),
        );
        userRepository.save(newUser);
    }
}

사용자 인증 정보를 데이터베이스에 저장할 수 있는 경우는 두 가지로, 사용자가 승인 과정이 끝날 때 애플리케이션으로 돌아올 때와 API 호출을 실행하는 경우입니다. 여기에서 이전에 세션 credentials 키를 설정했습니다.

/callback 경로 끝에서 saveUser를 호출합니다. 사용자의 이메일만 추출하는 대신 user_info 객체를 유지해야 합니다.

/** This is the end of the auth flow. We should save user info to the database. */
Userinfo userinfo = authService.getUserInfo(credentials);
saveUser(credentials, userinfo, session);

또한 API 호출 후에 사용자 인증 정보를 업데이트해야 합니다. 이 경우 업데이트된 사용자 인증 정보를 saveUser 메서드에 인수로 제공할 수 있습니다.

/** Save credentials in case access token was refreshed. */
saveUser(credentials, null, session);

사용자 인증 정보가 만료됨

갱신 토큰이 무효화되는 데에는 몇 가지 이유가 있습니다. 예를 들면 다음과 같습니다.

  • 갱신 토큰이 6개월 동안 사용되지 않았습니다.
  • 사용자가 앱의 액세스 권한을 취소합니다.
  • 사용자가 비밀번호를 변경합니다.
  • 사용자가 세션 제어 정책이 적용되는 Google Cloud 조직에 속해 있습니다.

사용자 인증 정보가 무효화되면 사용자에게 승인 흐름을 다시 보내 새 토큰을 획득합니다.

사용자 자동 라우팅

사용자가 이전에 애플리케이션을 승인했는지 감지하도록 부가기능 방문 경로를 수정합니다. 그렇다면 기본 부가기능 페이지로 연결합니다. 그렇지 않으면 로그인하라는 메시지를 표시합니다.

Python

애플리케이션이 시작될 때 데이터베이스 파일이 생성되었는지 확인합니다. 다음을 모듈 이니셜라이저 (예: 제공된 예에서 webapp/__init__.py) 또는 서버를 실행하는 기본 메서드에 삽입합니다.

# Initialize the database file if not created.
if not os.path.exists(DATABASE_FILE_NAME):
    db.create_all()

그런 다음 위에서 설명한 대로 메서드가 login_hinthd 쿼리 매개변수를 처리해야 합니다. 그런 다음 재방문자인 경우 매장 사용자 인증 정보를 로드합니다. login_hint를 받으면 재방문자임을 알 수 있습니다. 이 사용자에 대해 저장된 사용자 인증 정보를 가져와 세션에 로드합니다.

stored_credentials = get_credentials_from_storage(login_hint)

# If we have stored credentials, store them in the session.
if stored_credentials:
    # Load the client secrets file contents.
    client_secrets_dict = json.load(
        open(CLIENT_SECRETS_FILE)).get("web")

    # Update the credentials in the session.
    if not flask.session.get("credentials"):
        flask.session["credentials"] = {}

    flask.session["credentials"] = {
        "token": stored_credentials.access_token,
        "refresh_token": stored_credentials.refresh_token,
        "token_uri": client_secrets_dict["token_uri"],
        "client_id": client_secrets_dict["client_id"],
        "client_secret": client_secrets_dict["client_secret"],
        "scopes": SCOPES
    }

    # Set the username in the session.
    flask.session["username"] = stored_credentials.display_name

마지막으로 사용자 인증 정보가 없는 경우 사용자를 로그인 페이지로 라우트합니다. 문제가 있는 경우 기본 부가기능 페이지로 전달합니다.

if "credentials" not in flask.session or \
    flask.session["credentials"]["refresh_token"] is None:
    return flask.render_template("authorization.html")

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

Java

부가기능 방문 경로 (제공된 예에서는 /addon-discovery)로 이동합니다. 위에서 설명한 대로 여기에서 login_hinthd 쿼리 매개변수를 처리했습니다.

먼저 세션에 사용자 인증 정보가 있는지 확인합니다. 그렇지 않은 경우 startAuthFlow 메서드를 호출하여 인증 흐름을 통해 사용자를 라우팅합니다.

/** Check if the credentials exist in the session. The session could have
 *   been cleared when the user clicked the Sign-Out button, and the expected
 *   behavior after sign-out would be to display the sign-in page when the
 *   iframe is opened again. */
if (session.getAttribute("credentials") == null) {
    return startAuthFlow(model);
}

그런 다음 재방문자인 경우 H2 데이터베이스에서 사용자를 로드합니다. login_hint 쿼리 매개변수를 수신하면 재방문자입니다. 사용자가 H2 데이터베이스에 있는 경우 이전에 설정한 사용자 인증 정보 데이터 스토어에서 사용자 인증 정보를 로드하고 세션에서 사용자 인증 정보를 설정합니다. 사용자 인증 정보 데이터 저장소에서 사용자 인증 정보를 가져오지 않은 경우 startAuthFlow를 호출하여 사용자를 인증 흐름을 통해 라우팅합니다.

/** At this point, we know that credentials exist in the session, but we
 *   should update the session credentials with the credentials in persistent
 *   storage in case they were refreshed. If the credentials in persistent
 *   storage are null, we should navigate the user to the authorization flow
 *   to obtain persisted credentials. */

User storedUser = getUser(login_hint);

if (storedUser != null) {
    Credential credential = authService.loadFromCredentialDataStore(login_hint);
    if (credential != null) {
        session.setAttribute("credentials", credential);
    } else {
        return startAuthFlow(model);
    }
}

마지막으로 사용자를 부가기능 방문 페이지로 라우트합니다.

/** Finally, if there are credentials in the session and in persistent
 *   storage, direct the user to the addon-discovery page. */
return "addon-discovery";

부가기능 테스트

교사 테스트 사용자 중 하나로 Google 클래스룸에 로그인합니다. 수업 과제 탭으로 이동하여 새 과제를 만듭니다. 텍스트 영역 아래의 부가기능 버튼을 클릭한 다음 부가기능을 선택합니다. iframe이 열리고 부가기능이 GWM SDK의 앱 구성 페이지에서 지정한 첨부파일 설정 URI를 로드합니다.

수고하셨습니다 다음 단계인 연결 만들기 및 사용자 역할 식별을 진행할 준비가 되었습니다.