반복 로그인 처리

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

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

이 워크스루에서는 다음을 완료합니다.

  • 사용자 인증 정보의 영구 스토리지를 구현합니다.
  • login_hint 부가기능 쿼리 매개변수를 검색하고 평가합니다. 이 ID는 로그인한 사용자의 고유한 Google ID 번호입니다.

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

iframe 쿼리 매개변수 이해하기

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

탐색 URL에는 다음과 같은 5가지 쿼리 매개변수가 있습니다.

  • courseId: 현재 클래스룸 과정의 ID입니다.
  • itemId: 사용자가 수정하거나 만들고 있는 스트림 항목의 ID입니다.
  • itemType: 사용자가 만들거나 수정하는 스트림 항목의 종류로, courseWork, courseWorkMaterial 또는 announcement 중 하나입니다.
  • addOnToken: 특정 클래스룸 부가기능 작업을 승인하는 데 사용되는 토큰입니다.
  • login_hint: 현재 사용자의 Google ID입니다.

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

쿼리 매개변수에 액세스

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

Python

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

# If the login_hint query parameter is available, we'll store it in the session.
if flask.request.args.get("login_hint"):
    flask.session["login_hint"] = flask.request.args.get("login_hint")

login_hint (있는 경우)가 세션에 저장되어 있는지 확인합니다. 이는 이러한 값을 저장하기에 적절한 장소입니다. 값은 임시적이며 부가기능이 열리면 새 값을 수신합니다.

# 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.
login_hint = flask.session.get("login_hint")

# If there's still no login_hint query parameter, this must be their first
# time signing in, so send the user to the sign in page.
if login_hint is None:
    return start_auth_flow()

자바

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

/** Retrieve the login_hint query parameter from the request URL if present. */
String login_hint = request.getParameter("login_hint");

login_hint (있는 경우)가 세션에 저장되었는지 확인합니다. 이는 이러한 값을 저장하기에 적절한 장소입니다. 값은 임시적이며 부가기능이 열리면 새 값을 수신합니다.

/** If login_hint wasn't sent, use the values in the session. */
if (login_hint == null) {
    login_hint = (String) session.getAttribute("login_hint");
}

/** If the there is still no login_hint, route the user to the authorization
 *  page. */
if (login_hint == null) {
    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_hint 매개변수도 Google의 인증 서버에 전달해야 합니다. 이렇게 하면 인증 프로세스가 용이해집니다. 애플리케이션에서 인증을 시도하는 사용자를 알고 있는 경우, 서버는 힌트를 사용하여 로그인 양식의 이메일 필드를 미리 채워 로그인 흐름을 간소화합니다.

Python

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

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

자바

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

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

사용자 인증 정보의 영구 저장소 추가

부가기능이 로드될 때 login_hint가 쿼리 매개변수로 수신되면 사용자가 이미 애플리케이션의 승인 과정을 완료했음을 나타냅니다. 사용자에게 다시 로그인하라고 강요하는 대신 이전 사용자 인증 정보를 가져와야 합니다.

승인 흐름이 완료되면 새로고침 토큰을 받았습니다. 이 토큰을 저장합니다. 이 토큰은 Google API를 사용하는 데 필요한 단기 토큰인 액세스 토큰을 가져오는 데 재사용됩니다. 이전에 이러한 사용자 인증 정보를 세션에 저장했지만 재방문을 처리하려면 사용자 인증 정보를 저장해야 합니다.

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

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

Python

사용자 스키마 정의

User에는 다음과 같은 속성이 있습니다.

  • id: 사용자의 Google ID입니다. 이 값은 login_hint 쿼리 매개변수에 제공된 값과 일치해야 합니다.
  • display_name: 사용자의 성과 이름(예: '홍길동')
  • 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()

자바

사용자 스키마 정의

User에는 다음과 같은 속성이 있습니다.

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

모듈의 resources 디렉터리에 schema.sql 파일을 만듭니다. Spring은 이 파일을 읽고 그에 따라 데이터베이스의 스키마를 생성합니다. 테이블 이름 usersUser 속성 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
);

데이터베이스의 User 모델을 정의하는 Java 클래스를 만듭니다. 제공된 예에서는 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; }
}

데이터베이스에 대한 CRUD 작업을 처리하는 UserRepository.java라는 인터페이스를 만듭니다. 이 인터페이스는 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 구성은 h2라는 디렉터리를 만들고 그 안에 userdb 파일을 저장합니다. 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 객체를 만들고 반환하는 메서드를 만듭니다. 사용자 인증 정보를 저장하는 데이터 스토어입니다.

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

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

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

다음으로 getAndSaveCredentials(String authorizationCode) 메서드를 업데이트합니다. 이전에는 이 방법을 사용하여 사용자 인증 정보를 어디에도 저장하지 않고 얻었습니다. 사용자 ID로 색인이 생성된 Datastore에 사용자 인증 정보를 저장하는 메서드를 업데이트합니다.

사용자 ID는 id_token를 사용하여 TokenResponse 객체에서 얻을 수 있지만 먼저 확인이 되어야 합니다. 그러지 않으면 클라이언트 애플리케이션이 수정된 사용자 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)

자바

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)

자바

먼저 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_hint 쿼리 매개변수를 처리해야 합니다. 그런 다음 재방문자인 경우 스토어 사용자 인증 정보를 로드합니다. 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.")

자바

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

먼저 세션에 사용자 인증 정보가 있는지 확인합니다. 그렇지 않은 경우 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이 열리고 부가기능이 Google Workspace Marketplace SDK의 앱 구성 페이지에서 지정한 첨부파일 설정 URI를 로드합니다.

축하합니다. 이제 다음 단계인 첨부파일 만들기 및 사용자 역할 확인으로 진행할 수 있습니다.