这是 Google 课堂插件的第三个演示 演示系列视频
在本演示中,您将通过自动 检索用户先前授予的凭据。然后,您需要将用户 可立即发出 API 请求的页面。这是必填字段 Google 课堂插件的行为。
在本演示中,您将完成以下操作:
- 为用户凭据实现永久性存储。
- 检索并评估
login_hint
插件查询参数。这是一个 已登录用户的唯一 Google ID 编号。
完成后,您可以在您的 Web 应用中向用户全面授权,并向 Google API。
了解 iframe 查询参数
Google 课堂会在以下情况下加载插件的附件设置 URI:
。课堂
将多个 GET
查询参数附加到 URI;这些报告中包含
背景信息。例如,如果附件发现 URI 是
https://example.com/addon
,Google 课堂使用
来源网址设为
https://example.com/addon?courseId=XXX&itemId=YYY&itemType=courseWork&addOnToken=ZZZ
,
其中 XXX
、YYY
和 ZZZ
是字符串 ID。请参阅 iframe 指南
对此场景的详细说明
发现网址有五种可能的查询参数:
courseId
:当前 Google 课堂课程的 ID。itemId
:用户正在修改或创建的流项目的 ID。itemType
:用户正在创建或修改的信息流项目的类型,为以下之一:courseWork
、courseWorkMaterial
或announcement
。addOnToken
:用于向 Google 课堂插件操作。login_hint
:当前用户的 Google ID。
本演示介绍了 login_hint
。系统会根据是否
查询参数,如果缺失,则将其提供给授权流程,或者提供给
插件发现页面(如果有)。
访问查询参数
查询参数会以 URI 字符串的形式传递给 Web 应用。实体店 这些值它们在授权流程中使用 存储和检索用户相关信息。这些查询参数 在首次打开该插件时传递。
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()
Java
转到控制器类中的插件着陆路线
(在所提供示例中 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
) 中的授权路线
)。将 login_hint
参数添加到对
flow.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",
# The user will automatically be selected if we have the login_hint.
login_hint=flask.session.get("login_hint"),
Java
找到 AuthService.java
类中的 authorize()
方法。将
将 login_hint
作为参数添加到方法,并将 login_hint
和参数添加到授权网址构建工具中。
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
:用户的名字和姓氏,例如“Alex Smith”。email
:用户的电子邮件地址。portrait_url
:用户个人资料照片的网址。refresh_token
:之前获取的刷新令牌。
此示例使用 SQLite 实现存储,后者为
Python。它使用 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 指向 data.sqlite
与
main.py
文件。
接下来,导航到您的模块目录并创建一个新的 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
文件。春季
读取该文件并相应地生成数据库的架构。
使用表名称、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
注解,以指明这是一个可
已保存到数据库添加 @Table
注解以及
您在 schema.sql
中配置的相应表名称。
请注意,代码示例包含上述两种模型的构造函数和 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;
}
设置数据库
如需存储用户相关信息,请使用固有的 H2 数据库,
此数据库还会用于后续
存储其他与 Google 课堂相关的
信息。设置 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.username
和
spring.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;
}
更新 AuthService.java
中的 getFlow()
方法,使其包含
GoogleAuthorizationCodeFlow Builder()
中的setDataStoreFactory
方法并调用 getCredentialDataStore()
来设置数据存储区。
GoogleAuthorizationCodeFlow authorizationCodeFlow =
new GoogleAuthorizationCodeFlow.Builder(
HTTP_TRANSPORT,
JSON_FACTORY,
getClientSecrets(),
getScopes())
.setAccessType("offline")
.setDataStoreFactory(getCredentialDataStore())
.build();
接下来,更新 getAndSaveCredentials(String authorizationCode)
方法。
以前,此方法在未存储凭据的情况下获取凭据
位置。更新方法以将凭据存储在数据存储区中
已根据用户 ID 编入索引。
您可以使用TokenResponse
id_token
,但必须先对其进行验证。否则,
则应用可以通过向已修改的用户发送经过修改的用户
传递给服务器。建议您使用 Google API 客户端
库来验证 id_token
。有关详情,请参阅 [Google Identity 页面
验证 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();
更新对 flow.createAndStoreCredential
的调用以包含 userId
。
// Save the user id and credentials to the configured FileDataStoreFactory.
Credential credential = flow.createAndStoreCredential(tokenResponse, userId);
向 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
的方法。您在以下国家/地区获得了一个id
:
login_hint
查询参数,可用于检索特定用户
记录。
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);
凭据过期
请注意,有多种原因可能会导致刷新令牌失效。这些好处包括:
- 刷新令牌已有六个月未使用。
- 用户撤消应用的访问权限。
- 用户更改密码。
- 用户属于可控制会话的 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.")
Java
导航到您的插件着陆路线(/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 并且该插件会加载 附件设置 URI(在 Google Workspace Marketplace SDK 的应用配置页面。
恭喜!您已准备好继续执行下一步:创建附件 并确定用户的角色。