本文档介绍了 Web 服务器应用如何使用 Google API 客户端库或 Google OAuth 2.0 端点来进行 OAuth 2.0 授权,从而实现对 Google API 的访问。
OAuth 2.0 可让用户与应用共享特定数据,同时保持其用户名、密码和其他信息的私密性。例如,应用可以使用 OAuth 2.0 从用户处获取在其 Google 云端硬盘中存储文件的权限。
此 OAuth 2.0 流程专门用于用户授权。它专为可以存储机密信息和维护状态的应用而设计。在用户与应用互动期间或用户退出应用后,已正确授权的 Web 服务器应用可以访问 API。
Web 服务器应用通常还使用 服务账号来授权 API 请求,尤其是在调用 Cloud API 来访问基于项目的数据(而非用户专用数据)时。Web 服务器应用可以将服务账号与用户授权结合使用。
客户端库
本页面上特定于某语言的示例使用的是 Google API 客户端库来实现 OAuth 2.0 授权。如需运行代码示例,您必须先安装适用于您的语言的客户端库。
当您使用 Google API 客户端库处理应用的 OAuth 2.0 流程时,客户端库会执行许多应用原本需要自行处理的操作。例如,它会确定应用何时可以使用或刷新存储的访问令牌,以及应用何时必须重新获取用户同意。该客户端库还会生成正确的重定向网址,并帮助实现用于将授权代码换取访问令牌的重定向处理脚本。
适用于服务器端应用的 Google API 客户端库支持以下语言:
前提条件
为您的项目启用 API
任何调用 Google API 的应用都需要在 中启用这些 API。
如需为您的项目启用该 API,请按以下步骤操作:
- 中的 。
- 列出了所有可用的 API(按产品系列和热门程度分组)。如果列表中没有显示您要启用的 API,请使用搜索功能查找该 API,或点击其所属产品系列中的查看全部。
- 选择您要启用的 API,然后点击启用按钮。
创建授权凭据
任何使用 OAuth 2.0 访问 Google API 的应用都必须具有授权凭据,以便向 Google 的 OAuth 2.0 服务器表明应用的身份。以下步骤介绍了如何为项目创建凭据。然后,您的应用便可使用这些凭据访问您为该项目启用的 API。
- 点击创建客户端。
- 选择 Web 应用应用类型。
- 填写表单,然后点击创建。使用 PHP、Java、Python、Ruby 和 .NET 等语言和框架的应用必须指定已获授权的重定向 URI。重定向 URI 是 OAuth 2.0 服务器可以向其发送响应的端点。这些端点必须遵循 Google 的验证规则。
如需进行测试,您可以指定引用本地计算机的 URI,例如
http://localhost:8080
。请注意,本文档中的所有示例均使用http://localhost:8080
作为重定向 URI。我们建议您设计应用的身份验证端点,以免应用向网页上的其他资源公开授权代码。
创建凭据后,从 下载 client_secret.json 文件。将文件安全地存储在只有您的应用可以访问的位置。
确定访问权限范围
有了这一范围,您不但可以让应用仅请求访问所需的资源,而且还可以让用户控制其向您的应用授予的访问权限大小。因此,请求的范围数量与获得用户同意的可能性之间可能存在反比关系。
在开始实现 OAuth 2.0 授权之前,我们建议您确定应用需要访问权限的范围。
我们还建议您的应用通过增量授权流程请求授权范围访问权限,在该流程中,您的应用会请求在上下文中访问用户数据。此最佳实践有助于用户更轻松地了解您的应用为何需要请求的访问权限。
OAuth 2.0 API 范围文档包含您可能用来访问 Google API 的完整范围列表。
特定于语言的要求
如需运行本文档中的任何代码示例,您需要拥有 Google 账号、能够访问互联网,以及拥有网络浏览器。如果您使用的是某个 API 客户端库,请参阅下文中特定于语言的要求。
PHP
如需运行本文档中的 PHP 代码示例,您需要满足以下条件:
- 安装了命令行界面 (CLI) 和 JSON 扩展程序的 PHP 8.0 或更高版本。
- Composer 依赖项管理工具。
-
PHP 版 Google API 客户端库:
composer require google/apiclient:^2.15.0
如需了解详情,请参阅 PHP 版 Google API 客户端库。
Python
如需运行本文档中的 Python 代码示例,您需要满足以下条件:
- Python 3.7 或更高版本
- pip 软件包管理工具。
- Python 版 Google API 客户端库 2.0 版:
pip install --upgrade google-api-python-client
- 用于用户授权的
google-auth
、google-auth-oauthlib
和google-auth-httplib2
。pip install --upgrade google-auth google-auth-oauthlib google-auth-httplib2
- Flask Python Web 应用框架。
pip install --upgrade flask
requests
HTTP 库。pip install --upgrade requests
如果您无法升级 Python 和关联的迁移指南,请查看 Google API Python 客户端库的版本说明。
Ruby
如需运行本文档中的 Ruby 代码示例,您需要:
- Ruby 2.6 或更高版本
-
Ruby 版 Google 身份验证库:
gem install googleauth
-
适用于云端硬盘和 Google 日历 API 的客户端库:
gem install google-apis-drive_v3 google-apis-calendar_v3
-
Sinatra Ruby Web 应用框架。
gem install sinatra
Node.js
如需运行本文档中的 Node.js 代码示例,您需要满足以下条件:
- Node.js 的维护 LTS、活跃 LTS 或当前版本。
-
Google API Node.js 客户端:
npm install googleapis crypto express express-session
HTTP/REST
您无需安装任何库即可直接调用 OAuth 2.0 端点。
获取 OAuth 2.0 访问令牌
以下步骤展示了您的应用如何与 Google 的 OAuth 2.0 服务器互动,以便征得用户同意代表用户执行 API 请求。您的应用必须先征得用户同意,然后才能执行需要用户授权的 Google API 请求。
以下列表简要总结了这些步骤:
- 您的应用会识别其所需的权限。
- 您的应用会将用户重定向到 Google,并附上请求的权限列表。
- 用户决定是否向您的应用授予权限。
- 您的应用会了解用户的决定。
- 如果用户授予了请求的权限,您的应用会检索代表用户发出 API 请求所需的令牌。
第 1 步:设置授权参数
第一步是创建授权请求。该请求会设置用于标识您的应用的参数,并定义系统会要求用户向您的应用授予的权限。
- 如果您使用 Google 客户端库进行 OAuth 2.0 身份验证和授权,则需要创建并配置用于定义这些参数的对象。
- 如果您直接调用 Google OAuth 2.0 端点,则需要生成一个网址并在该网址上设置参数。
以下标签页定义了 Web 服务器应用支持的授权参数。特定于某种语言的示例还展示了如何使用客户端库或授权库配置用于设置这些参数的对象。
PHP
以下代码段会创建一个 Google\Client()
对象,用于定义授权请求中的参数。
该对象使用 client_secret.json 文件中的信息来标识您的应用。(如需详细了解该文件,请参阅创建授权凭据。)该对象还会标识您的应用请求访问权限的范围以及应用身份验证端点的网址,该端点将处理来自 Google OAuth 2.0 服务器的响应。最后,代码会设置可选的 access_type
和 include_granted_scopes
参数。
例如,以下代码请求对用户的 Google 云端硬盘元数据和 Google 日历活动的离线只读访问权限:
use Google\Client; $client = new Client(); // Required, call the setAuthConfig function to load authorization credentials from // client_secret.json file. $client->setAuthConfig('client_secret.json'); // Required, to set the scope value, call the addScope function $client->addScope([Google\Service\Drive::DRIVE_METADATA_READONLY, Google\Service\Calendar::CALENDAR_READONLY]); // Required, call the setRedirectUri function to specify a valid redirect URI for the // provided client_id $client->setRedirectUri('http://' . $_SERVER['HTTP_HOST'] . '/oauth2callback.php'); // Recommended, offline access will give you both an access and refresh token so that // your app can refresh the access token without user interaction. $client->setAccessType('offline'); // Recommended, call the setState function. Using a state value can increase your assurance that // an incoming connection is the result of an authentication request. $client->setState($sample_passthrough_value); // Optional, if your application knows which user is trying to authenticate, it can use this // parameter to provide a hint to the Google Authentication Server. $client->setLoginHint('hint@example.com'); // Optional, call the setPrompt function to set "consent" will prompt the user for consent $client->setPrompt('consent'); // Optional, call the setIncludeGrantedScopes function with true to enable incremental // authorization $client->setIncludeGrantedScopes(true);
Python
以下代码段使用 google-auth-oauthlib.flow
模块构建授权请求。
该代码会构建一个 Flow
对象,该对象会使用您在创建授权凭据后下载的 client_secret.json 文件中的信息来标识您的应用。该对象还会标识您的应用请求访问权限的范围以及应用身份验证端点的网址,该端点将处理来自 Google OAuth 2.0 服务器的响应。最后,代码会设置可选的 access_type
和 include_granted_scopes
参数。
例如,以下代码请求对用户的 Google 云端硬盘元数据和日历活动的离线只读访问权限:
import google.oauth2.credentials import google_auth_oauthlib.flow # Required, call the from_client_secrets_file method to retrieve the client ID from a # client_secret.json file. The client ID (from that file) and access scopes are required. (You can # also use the from_client_config method, which passes the client configuration as it originally # appeared in a client secrets file but doesn't access the file itself.) flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file('client_secret.json', scopes=['https://www.googleapis.com/auth/drive.metadata.readonly', 'https://www.googleapis.com/auth/calendar.readonly']) # Required, indicate where the API server will redirect the user after the user completes # the authorization flow. The redirect URI is required. The value 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 = 'https://www.example.com/oauth2callback' # Generate URL for request to Google's OAuth 2.0 server. # Use kwargs to set optional request parameters. authorization_url, state = flow.authorization_url( # Recommended, 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', # Optional, enable incremental authorization. Recommended as a best practice. include_granted_scopes='true', # Optional, if your application knows which user is trying to authenticate, it can use this # parameter to provide a hint to the Google Authentication Server. login_hint='hint@example.com', # Optional, set prompt to 'consent' will prompt the user for consent prompt='consent')
Ruby
使用您创建的 client_secrets.json 文件在应用中配置客户端对象。配置客户端对象时,您需要指定应用需要访问的范围,以及应用的身份验证端点的网址,该端点将处理来自 OAuth 2.0 服务器的响应。
例如,以下代码请求对用户的 Google 云端硬盘元数据和 Google 日历活动的离线只读访问权限:
require 'googleauth' require 'googleauth/web_user_authorizer' require 'googleauth/stores/redis_token_store' require 'google/apis/drive_v3' require 'google/apis/calendar_v3' # Required, call the from_file method to retrieve the client ID from a # client_secret.json file. client_id = Google::Auth::ClientId.from_file('/path/to/client_secret.json') # Required, scope value # Access scopes for two non-Sign-In scopes: Read-only Drive activity and Google Calendar. scope = ['Google::Apis::DriveV3::AUTH_DRIVE_METADATA_READONLY', 'Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY'] # Required, Authorizers require a storage instance to manage long term persistence of # access and refresh tokens. token_store = Google::Auth::Stores::RedisTokenStore.new(redis: Redis.new) # Required, indicate where the API server will redirect the user after the user completes # the authorization flow. The redirect URI is required. The value 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. callback_uri = '/oauth2callback' # To use OAuth2 authentication, we need access to a CLIENT_ID, CLIENT_SECRET, AND REDIRECT_URI # from the client_secret.json file. To get these credentials for your application, visit # https://console.cloud.google.com/apis/credentials. authorizer = Google::Auth::WebUserAuthorizer.new(client_id, scope, token_store, callback_uri)
您的应用使用客户端对象执行 OAuth 2.0 操作,例如生成授权请求网址和将访问令牌应用于 HTTP 请求。
Node.js
以下代码段会创建一个 google.auth.OAuth2
对象,用于定义授权请求中的参数。
该对象使用 client_secret.json 文件中的信息来识别您的应用。如需向用户请求检索访问令牌的权限,您可以将用户重定向到意见征求页面。如需创建意见征求页面网址,请执行以下操作:
const {google} = require('googleapis'); const crypto = require('crypto'); const express = require('express'); const session = require('express-session'); /** * To use OAuth2 authentication, we need access to a CLIENT_ID, CLIENT_SECRET, AND REDIRECT_URI * from the client_secret.json file. To get these credentials for your application, visit * https://console.cloud.google.com/apis/credentials. */ const oauth2Client = new google.auth.OAuth2( YOUR_CLIENT_ID, YOUR_CLIENT_SECRET, YOUR_REDIRECT_URL ); // Access scopes for two non-Sign-In scopes: Read-only Drive activity and Google Calendar. const scopes = [ 'https://www.googleapis.com/auth/drive.metadata.readonly', 'https://www.googleapis.com/auth/calendar.readonly' ]; // Generate a secure random state value. const state = crypto.randomBytes(32).toString('hex'); // Store state in the session req.session.state = state; // Generate a url that asks permissions for the Drive activity and Google Calendar scope const authorizationUrl = oauth2Client.generateAuthUrl({ // 'online' (default) or 'offline' (gets refresh_token) access_type: 'offline', /** Pass in the scopes array defined above. * Alternatively, if only one scope is needed, you can pass a scope URL as a string */ scope: scopes, // Enable incremental authorization. Recommended as a best practice. include_granted_scopes: true, // Include the state parameter to reduce the risk of CSRF attacks. state: state });
重要提示:系统仅在首次授权时返回 refresh_token
。如需了解详情,请点击
此处。
HTTP/REST
Google 的 OAuth 2.0 端点位于 https://accounts.google.com/o/oauth2/v2/auth
。此端点只能通过 HTTPS 访问。系统会拒绝普通 HTTP 连接。
Google 授权服务器支持以下网站服务器应用查询字符串参数:
参数 | |||||||
---|---|---|---|---|---|---|---|
client_id |
必填
应用的客户端 ID。您可以在 中找到此值。 |
||||||
redirect_uri |
必填
确定 API 服务器在用户完成授权流程后将用户重定向到何处。此值必须与 OAuth 2.0 客户端的已获授权重定向 URI 之一完全匹配,该 URI 是在客户端的
中配置的。如果此值与所提供 请注意, |
||||||
response_type |
必填
确定 Google OAuth 2.0 端点是否返回授权代码。 将参数值设置为 |
||||||
scope |
必填
以空格分隔的范围列表,用于标识您的应用可以代表用户访问的资源。这些值用于填充 Google 向用户显示的意见征求页面。 有了这一范围,您不但可以让应用仅请求访问所需的资源,而且还可以让用户控制其向您的应用授予的访问权限大小。因此,请求的范围数量与获得用户同意的可能性之间存在反比关系。 我们建议您的应用尽可能在上下文中请求访问授权范围。通过渐进式授权请求在上下文中访问用户数据,您可以帮助用户更轻松地了解您的应用为何需要请求的访问权限。 |
||||||
access_type |
推荐
指示您的应用是否可以在用户不在浏览器中时刷新访问令牌。有效的参数值为 如果您的应用需要在用户不在浏览器中时刷新访问令牌,请将此值设置为 |
||||||
state |
推荐
指定应用用于在授权请求和授权服务器响应之间维护状态的任何字符串值。
用户同意或拒绝应用的访问请求后,服务器会返回您在 您可以将此参数用于多种用途,例如将用户定向到应用中的正确资源、发送 Nonce 以及减少跨网站请求伪造。由于 |
||||||
include_granted_scopes |
可选
让应用能够使用增量授权来请求在上下文中访问其他作用域。如果您将此参数的值设置为 |
||||||
enable_granular_consent |
可选
默认为 当 Google 为应用启用精细权限时,此参数将不再有任何影响。 |
||||||
login_hint |
可选
如果您的应用知道哪位用户正在尝试进行身份验证,则可以使用此参数向 Google 身份验证服务器提供提示。服务器会使用提示来简化登录流程,方法是预填充登录表单中的电子邮件地址字段,或选择适当的多账号登录会话。 将参数值设置为电子邮件地址或 |
||||||
prompt |
可选
以空格分隔且区分大小写的提示列表,供向用户显示。如果您未指定此参数,则系统仅会在您的项目首次请求访问权限时向用户显示提示。如需了解详情,请参阅 提示用户重新同意。 可能的值包括:
|
第 2 步:重定向到 Google 的 OAuth 2.0 服务器
将用户重定向到 Google 的 OAuth 2.0 服务器,以启动身份验证和授权流程。通常,当您的应用首次需要访问用户的数据时,就会发生这种情况。对于增量授权,当应用首次需要访问尚无权访问的其他资源时,也会执行此步骤。
PHP
- 生成一个网址,用于向 Google 的 OAuth 2.0 服务器请求访问权限:
$auth_url = $client->createAuthUrl();
- 将用户重定向到
$auth_url
:header('Location: ' . filter_var($auth_url, FILTER_SANITIZE_URL));
Python
以下示例展示了如何使用 Flask Web 应用框架将用户重定向到授权网址:
return flask.redirect(authorization_url)
Ruby
- 生成一个网址,用于向 Google 的 OAuth 2.0 服务器请求访问权限:
auth_uri = authorizer.get_authorization_url(request: request)
- 将用户重定向到
auth_uri
。
Node.js
-
使用第 1 步
generateAuthUrl
方法生成的网址authorizationUrl
向 Google 的 OAuth 2.0 服务器请求访问权限。 -
将用户重定向到
authorizationUrl
。res.redirect(authorizationUrl);
HTTP/REST
重定向到 Google 授权服务器的示例
以下示例网址包含换行符和空格,以便于阅读。
https://accounts.google.com/o/oauth2/v2/auth? scope=https%3A//www.googleapis.com/auth/drive.metadata.readonly%20https%3A//www.googleapis.com/auth/calendar.readonly& access_type=offline& include_granted_scopes=true& response_type=code& state=state_parameter_passthrough_value& redirect_uri=https%3A//oauth2.example.com/code& client_id=client_id
创建请求网址后,请将用户重定向到该网址。
Google 的 OAuth 2.0 服务器会验证用户身份,并请求用户同意您的应用访问所请求的范围。系统会使用您指定的重定向网址将响应发回给您的应用。
第 3 步:Google 提示用户同意
在此步骤中,用户决定是否向您的应用授予所请求的访问权限。在此阶段,Google 会显示一个意见征求窗口,其中显示应用的名称,以及请求权限来使用用户授权凭据进行访问的 Google API 服务,以及要授予的访问范围的摘要。然后,用户可以同意授予对应用请求的一个或多个范围的访问权限,也可以拒绝该请求。
在此阶段,您的应用无需执行任何操作,只需等待 Google 的 OAuth 2.0 服务器的响应,以确定是否已授予任何访问权限。下一步将介绍该响应。
错误
向 Google 的 OAuth 2.0 授权端点发出的请求可能会显示面向用户的错误消息,而不是预期的身份验证和授权流程。下面列出了常见的错误代码和建议的解决方法。
admin_policy_enforced
由于 Google Workspace 管理员的政策,Google 账号无法授权所请求的一个或多个镜重范围。如需详细了解管理员如何限制对所有镜重或敏感和受限镜重范围的访问,直至向您的 OAuth 客户端 ID 明确授予访问权限,请参阅 Google Workspace 管理中心帮助文章 控制哪些第三方应用和内部应用可以访问 Google Workspace 数据。
disallowed_useragent
授权端点显示在 Google 的 OAuth 2.0 政策所禁止的嵌入式用户代理中。
Android
Android 开发者在 android.webkit.WebView
中打开授权请求时可能会遇到此错误消息。
开发者应改为使用 Android 库,例如 Google 登录(适用于 Android)或 OpenID Foundation 的 AppAuth for Android。
当 Android 应用在嵌入式用户代理中打开常规网页链接,并且用户从您的网站导航到 Google 的 OAuth 2.0 授权端点时,Web 开发者可能会遇到此错误。开发者应允许通用链接在操作系统的默认链接处理程序中打开,其中包括 Android 应用链接处理程序或默认浏览器应用。Android 自定义标签页库也是受支持的选项。
iOS
iOS 和 macOS 开发者在 WKWebView
中打开授权请求时可能会遇到此错误。
开发者应改用 iOS 库,例如 Google 登录(适用于 iOS)或 OpenID Foundation 的 AppAuth(适用于 iOS)。
当 iOS 或 macOS 应用在嵌入式用户代理中打开常规网页链接,并且用户从您的网站导航到 Google 的 OAuth 2.0 授权端点时,Web 开发者可能会遇到此错误。开发者应允许通用链接在操作系统的默认链接处理程序中打开,包括 Universal Links 处理程序或默认浏览器应用。SFSafariViewController
库也是受支持的选项。
org_internal
请求中的 OAuth 客户端 ID 属于一个项目,该项目会限制对特定 Google Cloud 组织中的 Google 账号的访问权限。如需详细了解此配置选项,请参阅“设置 OAuth 权限请求页面”帮助文章中的用户类型部分。
invalid_client
OAuth 客户端密钥不正确。查看 OAuth 客户端配置,包括用于此请求的客户端 ID 和密钥。
invalid_grant
刷新访问令牌或使用增量授权时,令牌可能已过期或已失效。 再次验证用户身份,并征得用户同意以获取新令牌。如果您仍然看到此错误,请确保您的应用已正确配置,并且您在请求中使用的是正确的令牌和参数。否则,用户账号可能已被删除或停用。
redirect_uri_mismatch
授权请求中传递的 redirect_uri
与 OAuth 客户端 ID 的已获授权的重定向 URI 不匹配。在
中查看已获授权的重定向 URI。
redirect_uri
参数可能指已废弃且不再受支持的 OAuth 带外 (OOB) 流程。请参阅迁移指南,更新您的集成。
invalid_request
您提交的请求有问题。这可能由以下多种原因导致:
- 请求的格式不正确
- 请求缺少必需参数
- 请求使用 Google 不支持的授权方法。验证您的 OAuth 集成是否使用了推荐的集成方法
第 4 步:处理 OAuth 2.0 服务器响应
OAuth 2.0 服务器会使用请求中指定的网址响应应用的访问请求。
如果用户批准了访问请求,响应中就会包含授权代码。如果用户未批准请求,响应中会包含错误消息。返回给网络服务器的授权代码或错误消息会显示在查询字符串中,如下所示:
错误响应:
https://oauth2.example.com/auth?error=access_denied
授权代码响应:
https://oauth2.example.com/auth?code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7
OAuth 2.0 服务器响应示例
您可以点击以下示例网址来测试此流程,该网址会请求读取权限,以查看您 Google 云端硬盘中文件的元数据,以及查看您 Google 日历活动的读取权限:
https://accounts.google.com/o/oauth2/v2/auth? scope=https%3A//www.googleapis.com/auth/drive.metadata.readonly%20https%3A//www.googleapis.com/auth/calendar.readonly& access_type=offline& include_granted_scopes=true& response_type=code& state=state_parameter_passthrough_value& redirect_uri=https%3A//oauth2.example.com/code& client_id=client_id
完成 OAuth 2.0 流程后,您应该会重定向到 http://localhost/oauth2callback
,除非您的本地机器在该地址上提供文件,否则系统可能会返回 404 NOT FOUND
错误。下一步将详细介绍在用户重定向回您的应用时 URI 中返回的信息。
第 5 步:使用授权代码换取刷新令牌和访问令牌
网站服务器收到授权代码后,可以用该授权代码来换取访问令牌。
PHP
如需将授权代码兑换为访问令牌,请使用 fetchAccessTokenWithAuthCode
方法:
$access_token = $client->fetchAccessTokenWithAuthCode($_GET['code']);
Python
在回调页面上,使用 google-auth
库验证授权服务器响应。然后,使用 flow.fetch_token
方法将该响应中的授权代码换成访问令牌:
state = flask.session['state'] flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file( 'client_secret.json', scopes=['https://www.googleapis.com/auth/drive.metadata.readonly'], state=state) flow.redirect_uri = flask.url_for('oauth2callback', _external=True) authorization_response = flask.request.url flow.fetch_token(authorization_response=authorization_response) # Store the credentials in the session. # ACTION ITEM for developers: # Store user's access and refresh tokens in your data store if # incorporating this code into your real app. 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, 'granted_scopes': credentials.granted_scopes}
Ruby
在回调页面上,使用 googleauth
库验证授权服务器响应。使用 authorizer.handle_auth_callback_deferred
方法保存授权代码,并重定向回最初请求授权的网址。这会通过将结果暂时存储在用户的会话中来推迟代码交换。
target_url = Google::Auth::WebUserAuthorizer.handle_auth_callback_deferred(request) redirect target_url
Node.js
如需使用授权代码兑换访问令牌,请使用 getToken
方法:
const url = require('url'); // Receive the callback from Google's OAuth 2.0 server. app.get('/oauth2callback', async (req, res) => { let q = url.parse(req.url, true).query; if (q.error) { // An error response e.g. error=access_denied console.log('Error:' + q.error); } else if (q.state !== req.session.state) { //check state value console.log('State mismatch. Possible CSRF attack'); res.end('State mismatch. Possible CSRF attack'); } else { // Get access and refresh tokens (if access_type is offline) let { tokens } = await oauth2Client.getToken(q.code); oauth2Client.setCredentials(tokens); });
HTTP/REST
如需使用授权代码交换访问令牌,请调用 https://oauth2.googleapis.com/token
端点并设置以下参数:
字段 | |
---|---|
client_id |
从 获取的客户端 ID。 |
client_secret |
从 获取的客户端密钥。 |
code |
从初始请求返回的授权代码。 |
grant_type |
如 OAuth 2.0 规范中所定义,此字段的值必须设置为 authorization_code 。 |
redirect_uri |
为给定 client_id 在
中为您的项目列出的重定向 URI 之一。 |
以下代码段显示了一个示例请求:
POST /token HTTP/1.1 Host: oauth2.googleapis.com Content-Type: application/x-www-form-urlencoded code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7& client_id=your_client_id& client_secret=your_client_secret& redirect_uri=https%3A//oauth2.example.com/code& grant_type=authorization_code
Google 会通过返回包含短时有效访问令牌和刷新令牌的 JSON 对象来响应此请求。
请注意,只有当您的应用在向 Google 授权服务器发出的初始请求中将 access_type
参数设置为 offline
时,系统才会返回刷新令牌。
响应包含以下字段:
字段 | |
---|---|
access_token |
您的应用发送的用于授权 Google API 请求的令牌。 |
expires_in |
访问令牌的剩余生命周期(以秒为单位)。 |
refresh_token |
您可以使用此令牌获取新的访问令牌。刷新令牌在用户撤消访问权限之前有效。
再次强调,只有当您在向 Google 授权服务器发出的初始请求中将 access_type 参数设置为 offline 时,此字段才会出现在此响应中。
|
scope |
access_token 授予的访问权限范围,表示为以空格分隔且区分大小写的字符串列表。 |
token_type |
返回的令牌类型。此时,此字段的值始终设置为 Bearer 。 |
以下代码段显示了示例响应:
{ "access_token": "1/fFAGRNJru1FTz70BzhT3Zg", "expires_in": 3920, "token_type": "Bearer", "scope": "https://www.googleapis.com/auth/drive.metadata.readonly https://www.googleapis.com/auth/calendar.readonly", "refresh_token": "1//xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI" }
错误
在将授权代码换成访问令牌时,您可能会遇到以下错误,而不是预期的响应。下面列出了常见的错误代码和建议的解决方法。
invalid_grant
提供的授权代码无效或格式有误。重启 OAuth 流程以请求新的代码,以便再次提示用户同意。
第 6 步:查看用户授予了哪些权限范围
同时请求多个范围时,用户可能不会授予您的应用请求的所有范围。您的应用应始终检查用户授予了哪些范围,并通过停用相关功能来处理任何范围被拒的情况。如需了解详情,请参阅如何处理精细权限。
PHP
如需检查用户已授予哪些范围,请使用 getGrantedScope()
方法:
// Space-separated string of granted scopes if it exists, otherwise null. $granted_scopes = $client->getOAuth2Service()->getGrantedScope(); // Determine which scopes user granted and build a dictionary $granted_scopes_dict = [ 'Drive' => str_contains($granted_scopes, Google\Service\Drive::DRIVE_METADATA_READONLY), 'Calendar' => str_contains($granted_scopes, Google\Service\Calendar::CALENDAR_READONLY) ];
Python
返回的 credentials
对象具有 granted_scopes
属性,该属性是用户向您的应用授予的范围的列表。
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, 'granted_scopes': credentials.granted_scopes}
以下函数会检查用户已向您的应用授予哪些范围。
def check_granted_scopes(credentials): features = {} if 'https://www.googleapis.com/auth/drive.metadata.readonly' in credentials['granted_scopes']: features['drive'] = True else: features['drive'] = False if 'https://www.googleapis.com/auth/calendar.readonly' in credentials['granted_scopes']: features['calendar'] = True else: features['calendar'] = False return features
Ruby
一次性请求多个镜重时,请通过 credentials
对象的 scope
属性检查系统授予了哪些镜重。
# User authorized the request. Now, check which scopes were granted. if credentials.scope.include?(Google::Apis::DriveV3::AUTH_DRIVE_METADATA_READONLY) # User authorized read-only Drive activity permission. # Calling the APIs, etc else # User didn't authorize read-only Drive activity permission. # Update UX and application accordingly end # Check if user authorized Calendar read permission. if credentials.scope.include?(Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY) # User authorized Calendar read permission. # Calling the APIs, etc. else # User didn't authorize Calendar read permission. # Update UX and application accordingly end
Node.js
一次性请求多个镜重时,请通过 tokens
对象的 scope
属性检查系统授予了哪些镜重。
// User authorized the request. Now, check which scopes were granted. if (tokens.scope.includes('https://www.googleapis.com/auth/drive.metadata.readonly')) { // User authorized read-only Drive activity permission. // Calling the APIs, etc. } else { // User didn't authorize read-only Drive activity permission. // Update UX and application accordingly } // Check if user authorized Calendar read permission. if (tokens.scope.includes('https://www.googleapis.com/auth/calendar.readonly')) { // User authorized Calendar read permission. // Calling the APIs, etc. } else { // User didn't authorize Calendar read permission. // Update UX and application accordingly }
HTTP/REST
如需检查用户是否已向您的应用授予对特定范围的访问权限,请检查访问令牌响应中的 scope
字段。access_token 授予的访问范围,表示为以空格分隔且区分大小写的字符串列表。
例如,以下示例访问令牌响应表明,用户已向您的应用授予对只读云端硬盘活动和日历活动权限的访问权限:
{ "access_token": "1/fFAGRNJru1FTz70BzhT3Zg", "expires_in": 3920, "token_type": "Bearer", "scope": "https://www.googleapis.com/auth/drive.metadata.readonly https://www.googleapis.com/auth/calendar.readonly", "refresh_token": "1//xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI" }
调用 Google API
PHP
如需使用访问令牌调用 Google API,请完成以下步骤:
- 如果您需要将访问令牌应用于新的
Google\Client
对象(例如,如果您将访问令牌存储在用户会话中),请使用setAccessToken
方法:$client->setAccessToken($access_token);
- 为要调用的 API 构建服务对象。您可以通过向要调用的 API 的构造函数提供已获授权的
Google\Client
对象来构建服务对象。例如,如需调用 Drive API,请执行以下操作:$drive = new Google\Service\Drive($client);
- 使用
服务对象提供的接口向 API 服务发出请求。
例如,如需列出已通过身份验证的用户的 Google 云端硬盘中的文件,请执行以下操作:
$files = $drive->files->listFiles(array());
Python
获取访问令牌后,您的应用可以使用该令牌代表给定用户账号或服务账号授权 API 请求。使用特定于用户的授权凭据为您要调用的 API 构建服务对象,然后使用该对象发出已获授权的 API 请求。
- 为要调用的 API 构建服务对象。您可以通过调用
googleapiclient.discovery
库的build
方法(并提供 API 的名称和版本以及用户凭据)来构建服务对象: 例如,如需调用 Drive API 版本 3,请执行以下操作:from googleapiclient.discovery import build drive = build('drive', 'v2', credentials=credentials)
- 使用服务对象提供的接口向 API 服务发出请求。
例如,如需列出已通过身份验证的用户的 Google 云端硬盘中的文件,请执行以下操作:
files = drive.files().list().execute()
Ruby
获取访问令牌后,您的应用可以使用该令牌代表给定用户账号或服务账号发出 API 请求。使用特定于用户的授权凭据为您要调用的 API 构建服务对象,然后使用该对象发出已获授权的 API 请求。
- 为要调用的 API 构建服务对象。
例如,如需调用 Drive API 版本 3,请执行以下操作:
drive = Google::Apis::DriveV3::DriveService.new
- 在服务上设置凭据:
drive.authorization = credentials
- 使用服务对象提供的接口向 API 服务发出请求。
例如,如需列出已通过身份验证的用户的 Google 云端硬盘中的文件,请执行以下操作:
files = drive.list_files
或者,您也可以按方法提供授权,方法是向方法提供 options
参数:
files = drive.list_files(options: { authorization: credentials })
Node.js
获取访问令牌并将其设置为 OAuth2
对象后,使用该对象调用 Google API。您的应用可以使用该令牌代表给定用户账号或服务账号授权 API 请求。为要调用的 API 构建服务对象。
例如,以下代码使用 Google 云端硬盘 API 列出用户云端硬盘中的文件名。
const { google } = require('googleapis'); // Example of using Google Drive API to list filenames in user's Drive. const drive = google.drive('v3'); drive.files.list({ auth: oauth2Client, pageSize: 10, fields: 'nextPageToken, files(id, name)', }, (err1, res1) => { if (err1) return console.log('The API returned an error: ' + err1); const files = res1.data.files; if (files.length) { console.log('Files:'); files.map((file) => { console.log(`${file.name} (${file.id})`); }); } else { console.log('No files found.'); } });
HTTP/REST
应用获取访问令牌后,如果已授予 API 所需的访问范围,您就可以使用该令牌代表给定用户账号调用 Google API。为此,请通过添加 access_token
查询参数或 Authorization
HTTP 标头 Bearer
值,在向 API 发出的请求中添加访问令牌。请尽可能使用 HTTP 标头,因为查询字符串通常会显示在服务器日志中。在大多数情况下,您可以使用客户端库设置对 Google API 的调用(例如,调用云端硬盘 Files API 时)。
您可以在 OAuth 2.0 Playground 中试用所有 Google API 并查看其范围。
HTTP GET 示例
使用 Authorization: Bearer
HTTP 标头调用
drive.files
端点(即云端硬盘文件 API)的代码可能如下所示。请注意,您需要指定自己的访问令牌:
GET /drive/v2/files HTTP/1.1 Host: www.googleapis.com Authorization: Bearer access_token
以下是使用 access_token
查询字符串参数对已验证用户调用同一 API 的示例:
GET https://www.googleapis.com/drive/v2/files?access_token=access_token
curl
示例
您可以使用 curl
命令行应用测试这些命令。下面是一个使用 HTTP 标头选项(首选)的示例:
curl -H "Authorization: Bearer access_token" https://www.googleapis.com/drive/v2/files
或者,您也可以使用查询字符串参数选项:
curl https://www.googleapis.com/drive/v2/files?access_token=access_token
完整示例
以下示例会在用户完成身份验证并同意应用访问其云端硬盘元数据后,以 JSON 格式输出用户 Google 云端硬盘中的文件列表。
PHP
如需运行此示例,请执行以下操作:
- 在 中,将本地计算机的网址添加到重定向网址列表中。例如,添加
http://localhost:8080
。 - 创建一个新目录并切换到该目录。例如:
mkdir ~/php-oauth2-example cd ~/php-oauth2-example
- 使用 Composer 安装 PHP 版 Google API 客户端库:
composer require google/apiclient:^2.15.0
- 创建包含以下内容的
index.php
和oauth2callback.php
文件。 - 使用 PHP 的内置测试 Web 服务器运行该示例:
php -S localhost:8080 ~/php-oauth2-example
index.php
<?php require_once __DIR__.'/vendor/autoload.php'; session_start(); $client = new Google\Client(); $client->setAuthConfig('client_secret.json'); // User granted permission as an access token is in the session. if (isset($_SESSION['access_token']) && $_SESSION['access_token']) { $client->setAccessToken($_SESSION['access_token']); // Check if user granted Drive permission if ($_SESSION['granted_scopes_dict']['Drive']) { echo "Drive feature is enabled."; echo "</br>"; $drive = new Drive($client); $files = array(); $response = $drive->files->listFiles(array()); foreach ($response->files as $file) { echo "File: " . $file->name . " (" . $file->id . ")"; echo "</br>"; } } else { echo "Drive feature is NOT enabled."; echo "</br>"; } // Check if user granted Calendar permission if ($_SESSION['granted_scopes_dict']['Calendar']) { echo "Calendar feature is enabled."; echo "</br>"; } else { echo "Calendar feature is NOT enabled."; echo "</br>"; } } else { // Redirect users to outh2call.php which redirects users to Google OAuth 2.0 $redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . '/oauth2callback.php'; header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL)); } ?>
oauth2callback.php
<?php require_once __DIR__.'/vendor/autoload.php'; session_start(); $client = new Google\Client(); // Required, call the setAuthConfig function to load authorization credentials from // client_secret.json file. $client->setAuthConfigFile('client_secret.json'); $client->setRedirectUri('http://' . $_SERVER['HTTP_HOST']. $_SERVER['PHP_SELF']); // Required, to set the scope value, call the addScope function. $client->addScope([Google\Service\Drive::DRIVE_METADATA_READONLY, Google\Service\Calendar::CALENDAR_READONLY]); // Enable incremental authorization. Recommended as a best practice. $client->setIncludeGrantedScopes(true); // Recommended, offline access will give you both an access and refresh token so that // your app can refresh the access token without user interaction. $client->setAccessType("offline"); // Generate a URL for authorization as it doesn't contain code and error if (!isset($_GET['code']) && !isset($_GET['error'])) { // Generate and set state value $state = bin2hex(random_bytes(16)); $client->setState($state); $_SESSION['state'] = $state; // Generate a url that asks permissions. $auth_url = $client->createAuthUrl(); header('Location: ' . filter_var($auth_url, FILTER_SANITIZE_URL)); } // User authorized the request and authorization code is returned to exchange access and // refresh tokens. if (isset($_GET['code'])) { // Check the state value if (!isset($_GET['state']) || $_GET['state'] !== $_SESSION['state']) { die('State mismatch. Possible CSRF attack.'); } // Get access and refresh tokens (if access_type is offline) $token = $client->fetchAccessTokenWithAuthCode($_GET['code']); /** Save access and refresh token to the session variables. * ACTION ITEM: In a production app, you likely want to save the * refresh token in a secure persistent storage instead. */ $_SESSION['access_token'] = $token; $_SESSION['refresh_token'] = $client->getRefreshToken(); // Space-separated string of granted scopes if it exists, otherwise null. $granted_scopes = $client->getOAuth2Service()->getGrantedScope(); // Determine which scopes user granted and build a dictionary $granted_scopes_dict = [ 'Drive' => str_contains($granted_scopes, Google\Service\Drive::DRIVE_METADATA_READONLY), 'Calendar' => str_contains($granted_scopes, Google\Service\Calendar::CALENDAR_READONLY) ]; $_SESSION['granted_scopes_dict'] = $granted_scopes_dict; $redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . '/'; header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL)); } // An error response e.g. error=access_denied if (isset($_GET['error'])) { echo "Error: ". $_GET['error']; } ?>
Python
此示例使用 Flask 框架。它会在 http://localhost:8080
上运行 Web 应用,以便您测试 OAuth 2.0 流程。如果您访问该网址,应该会看到五个链接:
- 调用 Drive API:此链接指向一个页面,该页面会在用户授予相应权限后尝试执行示例 API 请求。如有必要,它会启动授权流程。 如果成功,页面会显示 API 响应。
- 用于调用 Google 日历 API 的模拟页面:此链接指向一个模拟页面,该页面会在用户授予权限后尝试执行 Google 日历 API 请求示例。如有必要,它会启动授权流程。如果成功,页面会显示 API 响应。
- 直接测试身份验证流程:此链接指向的页面会尝试将用户引导至授权流程。应用请求权限,以便代表用户提交已获授权的 API 请求。
- 撤消当前凭据:此链接指向一个页面,用于 撤消用户已向应用授予的权限。
- 清除 Flask 会话凭据:此链接会清除存储在 Flask 会话中的授权凭据。这样,您就可以了解如果已向您的应用授予权限的用户尝试在新会话中执行 API 请求,会出现什么情况。此外,您还可以查看以下情况时应用会收到的 API 响应:用户撤消了授予应用的权限,而应用仍尝试使用已撤消的访问令牌授权请求。
# -*- coding: utf-8 -*- import os import flask import requests import google.oauth2.credentials import google_auth_oauthlib.flow import googleapiclient.discovery # This variable specifies the name of a file that contains the OAuth 2.0 # information for this application, including its client_id and client_secret. CLIENT_SECRETS_FILE = "client_secret.json" # The OAuth 2.0 access scope allows for access to the # authenticated user's account and requires requests to use an SSL connection. SCOPES = ['https://www.googleapis.com/auth/drive.metadata.readonly', 'https://www.googleapis.com/auth/calendar.readonly'] API_SERVICE_NAME = 'drive' API_VERSION = 'v2' app = flask.Flask(__name__) # Note: A secret key is included in the sample so that it works. # If you use this code in your application, replace this with a truly secret # key. See https://flask.palletsprojects.com/quickstart/#sessions. app.secret_key = 'REPLACE ME - this value is here as a placeholder.' @app.route('/') def index(): return print_index_table() @app.route('/drive') def drive_api_request(): if 'credentials' not in flask.session: return flask.redirect('authorize') features = flask.session['features'] if features['drive']: # Load credentials from the session. credentials = google.oauth2.credentials.Credentials( **flask.session['credentials']) drive = googleapiclient.discovery.build( API_SERVICE_NAME, API_VERSION, credentials=credentials) files = drive.files().list().execute() # Save credentials back to session in case access token was refreshed. # ACTION ITEM: In a production app, you likely want to save these # credentials in a persistent database instead. flask.session['credentials'] = credentials_to_dict(credentials) return flask.jsonify(**files) else: # User didn't authorize read-only Drive activity permission. # Update UX and application accordingly return '<p>Drive feature is not enabled.</p>' @app.route('/calendar') def calendar_api_request(): if 'credentials' not in flask.session: return flask.redirect('authorize') features = flask.session['features'] if features['calendar']: # User authorized Calendar read permission. # Calling the APIs, etc. return ('<p>User granted the Google Calendar read permission. '+ 'This sample code does not include code to call Calendar</p>') else: # User didn't authorize Calendar read permission. # Update UX and application accordingly return '<p>Calendar feature is not enabled.</p>' @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) # 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('oauth2callback', _external=True) 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 return flask.redirect(authorization_url) @app.route('/oauth2callback') def oauth2callback(): # Specify the state when creating the flow in the callback so that it can # verified in the authorization server response. 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('oauth2callback', _external=True) # Use the authorization server's response to fetch the OAuth 2.0 tokens. authorization_response = flask.request.url flow.fetch_token(authorization_response=authorization_response) # Store credentials in the session. # ACTION ITEM: In a production app, you likely want to save these # credentials in a persistent database instead. credentials = flow.credentials credentials = credentials_to_dict(credentials) flask.session['credentials'] = credentials # Check which scopes user granted features = check_granted_scopes(credentials) flask.session['features'] = features return flask.redirect('/') @app.route('/revoke') def revoke(): if 'credentials' not in flask.session: return ('You need to <a href="/authorize">authorize</a> before ' + 'testing the code 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'}) status_code = getattr(revoke, 'status_code') if status_code == 200: return('Credentials successfully revoked.' + print_index_table()) else: return('An error occurred.' + print_index_table()) @app.route('/clear') def clear_credentials(): if 'credentials' in flask.session: del flask.session['credentials'] return ('Credentials have been cleared.<br><br>' + print_index_table()) def credentials_to_dict(credentials): return {'token': credentials.token, 'refresh_token': credentials.refresh_token, 'token_uri': credentials.token_uri, 'client_id': credentials.client_id, 'client_secret': credentials.client_secret, 'granted_scopes': credentials.granted_scopes} def check_granted_scopes(credentials): features = {} if 'https://www.googleapis.com/auth/drive.metadata.readonly' in credentials['granted_scopes']: features['drive'] = True else: features['drive'] = False if 'https://www.googleapis.com/auth/calendar.readonly' in credentials['granted_scopes']: features['calendar'] = True else: features['calendar'] = False return features def print_index_table(): return ('<table>' + '<tr><td><a href="/test">Test an API request</a></td>' + '<td>Submit an API request and see a formatted JSON response. ' + ' Go through the authorization flow if there are no stored ' + ' credentials for the user.</td></tr>' + '<tr><td><a href="/authorize">Test the auth flow directly</a></td>' + '<td>Go directly to the authorization flow. If there are stored ' + ' credentials, you still might not be prompted to reauthorize ' + ' the application.</td></tr>' + '<tr><td><a href="/revoke">Revoke current credentials</a></td>' + '<td>Revoke the access token associated with the current user ' + ' session. After revoking credentials, if you go to the test ' + ' page, you should see an <code>invalid_grant</code> error.' + '</td></tr>' + '<tr><td><a href="/clear">Clear Flask session credentials</a></td>' + '<td>Clear the access token currently stored in the user session. ' + ' After clearing the token, if you <a href="/test">test the ' + ' API request</a> again, you should go back to the auth flow.' + '</td></tr></table>') if __name__ == '__main__': # When running locally, disable OAuthlib's HTTPs verification. # ACTION ITEM for developers: # When running in production *do not* leave this option enabled. os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' # This disables the requested scopes and granted scopes check. # If users only grant partial request, the warning would not be thrown. os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1' # Specify a hostname and port that are set as a valid redirect URI # for your API project in the . app.run('localhost', 8080, debug=True)
Ruby
此示例使用 Sinatra 框架。
require 'googleauth' require 'googleauth/web_user_authorizer' require 'googleauth/stores/redis_token_store' require 'google/apis/drive_v3' require 'google/apis/calendar_v3' require 'sinatra' configure do enable :sessions # Required, call the from_file method to retrieve the client ID from a # client_secret.json file. set :client_id, Google::Auth::ClientId.from_file('/path/to/client_secret.json') # Required, scope value # Access scopes for two non-Sign-In scopes: Read-only Drive activity and Google Calendar. scope = ['Google::Apis::DriveV3::AUTH_DRIVE_METADATA_READONLY', 'Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY'] # Required, Authorizers require a storage instance to manage long term persistence of # access and refresh tokens. set :token_store, Google::Auth::Stores::RedisTokenStore.new(redis: Redis.new) # Required, indicate where the API server will redirect the user after the user completes # the authorization flow. The redirect URI is required. The value 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. set :callback_uri, '/oauth2callback' # To use OAuth2 authentication, we need access to a CLIENT_ID, CLIENT_SECRET, AND REDIRECT_URI # from the client_secret.json file. To get these credentials for your application, visit # https://console.cloud.google.com/apis/credentials. set :authorizer, Google::Auth::WebUserAuthorizer.new(settings.client_id, settings.scope, settings.token_store, callback_uri: settings.callback_uri) end get '/' do # NOTE: Assumes the user is already authenticated to the app user_id = request.session['user_id'] # Fetch stored credentials for the user from the given request session. # nil if none present credentials = settings.authorizer.get_credentials(user_id, request) if credentials.nil? # Generate a url that asks the user to authorize requested scope(s). # Then, redirect user to the url. redirect settings.authorizer.get_authorization_url(request: request) end # User authorized the request. Now, check which scopes were granted. if credentials.scope.include?(Google::Apis::DriveV3::AUTH_DRIVE_METADATA_READONLY) # User authorized read-only Drive activity permission. # Example of using Google Drive API to list filenames in user's Drive. drive = Google::Apis::DriveV3::DriveService.new files = drive.list_files(options: { authorization: credentials }) "<pre>#{JSON.pretty_generate(files.to_h)}</pre>" else # User didn't authorize read-only Drive activity permission. # Update UX and application accordingly end # Check if user authorized Calendar read permission. if credentials.scope.include?(Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY) # User authorized Calendar read permission. # Calling the APIs, etc. else # User didn't authorize Calendar read permission. # Update UX and application accordingly end end # Receive the callback from Google's OAuth 2.0 server. get '/oauth2callback' do # Handle the result of the oauth callback. Defers the exchange of the code by # temporarily stashing the results in the user's session. target_url = Google::Auth::WebUserAuthorizer.handle_auth_callback_deferred(request) redirect target_url end
Node.js
如需运行此示例,请执行以下操作:
-
在 中,将本地计算机的网址添加到重定向网址列表中。例如,添加
http://localhost
。 - 确保您已安装维护 LTS、活跃 LTS 或当前版本的 Node.js。
-
创建一个新目录并切换到该目录。例如:
mkdir ~/nodejs-oauth2-example cd ~/nodejs-oauth2-example
-
使用 npm 安装适用于 Node.js 的 Google API 客户端库:
npm install googleapis
-
创建包含以下内容的
main.js
文件。 -
运行示例:
node .\main.js
main.js
const http = require('http'); const https = require('https'); const url = require('url'); const { google } = require('googleapis'); const crypto = require('crypto'); const express = require('express'); const session = require('express-session'); /** * To use OAuth2 authentication, we need access to a CLIENT_ID, CLIENT_SECRET, AND REDIRECT_URI. * To get these credentials for your application, visit * https://console.cloud.google.com/apis/credentials. */ const oauth2Client = new google.auth.OAuth2( YOUR_CLIENT_ID, YOUR_CLIENT_SECRET, YOUR_REDIRECT_URL ); // Access scopes for two non-Sign-In scopes: Read-only Drive activity and Google Calendar. const scopes = [ 'https://www.googleapis.com/auth/drive.metadata.readonly', 'https://www.googleapis.com/auth/calendar.readonly' ]; /* Global variable that stores user credential in this code example. * ACTION ITEM for developers: * Store user's refresh token in your data store if * incorporating this code into your real app. * For more information on handling refresh tokens, * see https://github.com/googleapis/google-api-nodejs-client#handling-refresh-tokens */ let userCredential = null; async function main() { const app = express(); app.use(session({ secret: 'your_secure_secret_key', // Replace with a strong secret resave: false, saveUninitialized: false, })); // Example on redirecting user to Google's OAuth 2.0 server. app.get('/', async (req, res) => { // Generate a secure random state value. const state = crypto.randomBytes(32).toString('hex'); // Store state in the session req.session.state = state; // Generate a url that asks permissions for the Drive activity and Google Calendar scope const authorizationUrl = oauth2Client.generateAuthUrl({ // 'online' (default) or 'offline' (gets refresh_token) access_type: 'offline', /** Pass in the scopes array defined above. * Alternatively, if only one scope is needed, you can pass a scope URL as a string */ scope: scopes, // Enable incremental authorization. Recommended as a best practice. include_granted_scopes: true, // Include the state parameter to reduce the risk of CSRF attacks. state: state }); res.redirect(authorizationUrl); }); // Receive the callback from Google's OAuth 2.0 server. app.get('/oauth2callback', async (req, res) => { // Handle the OAuth 2.0 server response let q = url.parse(req.url, true).query; if (q.error) { // An error response e.g. error=access_denied console.log('Error:' + q.error); } else if (q.state !== req.session.state) { //check state value console.log('State mismatch. Possible CSRF attack'); res.end('State mismatch. Possible CSRF attack'); } else { // Get access and refresh tokens (if access_type is offline) let { tokens } = await oauth2Client.getToken(q.code); oauth2Client.setCredentials(tokens); /** Save credential to the global variable in case access token was refreshed. * ACTION ITEM: In a production app, you likely want to save the refresh token * in a secure persistent database instead. */ userCredential = tokens; // User authorized the request. Now, check which scopes were granted. if (tokens.scope.includes('https://www.googleapis.com/auth/drive.metadata.readonly')) { // User authorized read-only Drive activity permission. // Example of using Google Drive API to list filenames in user's Drive. const drive = google.drive('v3'); drive.files.list({ auth: oauth2Client, pageSize: 10, fields: 'nextPageToken, files(id, name)', }, (err1, res1) => { if (err1) return console.log('The API returned an error: ' + err1); const files = res1.data.files; if (files.length) { console.log('Files:'); files.map((file) => { console.log(`${file.name} (${file.id})`); }); } else { console.log('No files found.'); } }); } else { // User didn't authorize read-only Drive activity permission. // Update UX and application accordingly } // Check if user authorized Calendar read permission. if (tokens.scope.includes('https://www.googleapis.com/auth/calendar.readonly')) { // User authorized Calendar read permission. // Calling the APIs, etc. } else { // User didn't authorize Calendar read permission. // Update UX and application accordingly } } }); // Example on revoking a token app.get('/revoke', async (req, res) => { // Build the string for the POST request let postData = "token=" + userCredential.access_token; // Options for POST request to Google's OAuth 2.0 server to revoke a token let postOptions = { host: 'oauth2.googleapis.com', port: '443', path: '/revoke', method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(postData) } }; // Set up the request const postReq = https.request(postOptions, function (res) { res.setEncoding('utf8'); res.on('data', d => { console.log('Response: ' + d); }); }); postReq.on('error', error => { console.log(error) }); // Post the request with data postReq.write(postData); postReq.end(); }); const server = http.createServer(app); server.listen(8080); } main().catch(console.error);
HTTP/REST
此 Python 示例使用 Flask 框架和 Requests 库演示了 OAuth 2.0 Web 流程。我们建议在此流程中使用 Python 版 Google API 客户端库。(“Python”标签页中的示例确实使用了客户端库。)
import json import flask import requests app = flask.Flask(__name__) # To get these credentials (CLIENT_ID CLIENT_SECRET) and for your application, visit # https://console.cloud.google.com/apis/credentials. CLIENT_ID = '123456789.apps.googleusercontent.com' CLIENT_SECRET = 'abc123' # Read from a file or environmental variable in a real app # Access scopes for two non-Sign-In scopes: Read-only Drive activity and Google Calendar. SCOPE = 'https://www.googleapis.com/auth/drive.metadata.readonly https://www.googleapis.com/auth/calendar.readonly' # Indicate where the API server will redirect the user after the user completes # the authorization flow. The redirect URI is required. The value 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. REDIRECT_URI = 'http://example.com/oauth2callback' @app.route('/') def index(): if 'credentials' not in flask.session: return flask.redirect(flask.url_for('oauth2callback')) credentials = json.loads(flask.session['credentials']) if credentials['expires_in'] <= 0: return flask.redirect(flask.url_for('oauth2callback')) else: # User authorized the request. Now, check which scopes were granted. if 'https://www.googleapis.com/auth/drive.metadata.readonly' in credentials['scope']: # User authorized read-only Drive activity permission. # Example of using Google Drive API to list filenames in user's Drive. headers = {'Authorization': 'Bearer {}'.format(credentials['access_token'])} req_uri = 'https://www.googleapis.com/drive/v2/files' r = requests.get(req_uri, headers=headers).text else: # User didn't authorize read-only Drive activity permission. # Update UX and application accordingly r = 'User did not authorize Drive permission.' # Check if user authorized Calendar read permission. if 'https://www.googleapis.com/auth/calendar.readonly' in credentials['scope']: # User authorized Calendar read permission. # Calling the APIs, etc. r += 'User authorized Calendar permission.' else: # User didn't authorize Calendar read permission. # Update UX and application accordingly r += 'User did not authorize Calendar permission.' return r @app.route('/oauth2callback') def oauth2callback(): if 'code' not in flask.request.args: state = str(uuid.uuid4()) flask.session['state'] = state # Generate a url that asks permissions for the Drive activity # and Google Calendar scope. Then, redirect user to the url. auth_uri = ('https://accounts.google.com/o/oauth2/v2/auth?response_type=code' '&client_id={}&redirect_uri={}&scope={}&state={}').format(CLIENT_ID, REDIRECT_URI, SCOPE, state) return flask.redirect(auth_uri) else: if 'state' not in flask.request.args or flask.request.args['state'] != flask.session['state']: return 'State mismatch. Possible CSRF attack.', 400 auth_code = flask.request.args.get('code') data = {'code': auth_code, 'client_id': CLIENT_ID, 'client_secret': CLIENT_SECRET, 'redirect_uri': REDIRECT_URI, 'grant_type': 'authorization_code'} # Exchange authorization code for access and refresh tokens (if access_type is offline) r = requests.post('https://oauth2.googleapis.com/token', data=data) flask.session['credentials'] = r.text return flask.redirect(flask.url_for('index')) if __name__ == '__main__': import uuid app.secret_key = str(uuid.uuid4()) app.debug = False app.run()
重定向 URI 验证规则
Google 会对重定向 URI 应用以下验证规则,以帮助开发者确保其应用的安全性。您的重定向 URI 必须遵循以下规则。如需了解下文中提及的域名、主机、路径、查询、架构和用户信息的定义,请参阅 RFC 3986 第 3 节。
验证规则 | |
---|---|
架构 |
重定向 URI 必须使用 HTTPS 架构,而非纯 HTTP。localhost URI(包括 localhost IP 地址 URI)不受此规则的约束。 |
主机 |
主机不能是原始 IP 地址。此规则不适用于 localhost IP 地址。 |
网域 |
“googleusercontent.com” 。goo.gl ),除非相应网域归应用所有。此外,如果拥有缩短网址域名的应用选择重定向到该域名,则重定向 URI 的路径中必须包含 “/google-callback/” ,或者以 “/google-callback” 结尾。 |
Userinfo |
重定向 URI 不得包含 userinfo 子组件。 |
路径 |
重定向 URI 不得包含路径遍历(也称为目录回溯),路径遍历由 |
查询 |
重定向 URI 不得包含打开的重定向。 |
fragment |
重定向 URI 不得包含 fragment 组件。 |
角色 |
重定向 URI 不得包含以下字符:
|
增量授权
在 OAuth 2.0 协议中,您的应用会请求访问资源的授权,这些资源由范围标识。在需要资源时请求授权被视为最佳用户体验做法。为了实现这种做法,Google 的授权服务器支持增量授权。借助此功能,您可以根据需要请求镜重,如果用户授予了新镜重权限,系统会返回一个授权代码,该代码可用于交换包含用户向项目授予的所有镜重范围的令牌。
例如,一款允许用户试听音乐曲目和创建混音的应用在登录时可能只需要很少的资源,可能只需要登录用户的姓名。不过,若要保存完成的混音,需要访问对方的 Google 云端硬盘。如果应用仅在实际需要时才请求访问用户的 Google 云端硬盘,大多数用户都会认为这是理所当然的。
在这种情况下,应用可能会在登录时请求 openid
和 profile
权限范围以执行基本登录,然后在首次请求保存混合时请求 https://www.googleapis.com/auth/drive.file
权限范围。
如需实现增量授权,您需要完成请求访问令牌的常规流程,但要确保授权请求包含之前授予的范围。通过这种方法,您的应用可以避免管理多个访问令牌。
以下规则适用于通过增量授权获取的访问令牌:
- 该令牌可用于访问与合并到新授权中的任何范围对应的资源。
- 当您使用组合授权的刷新令牌来获取访问令牌时,访问令牌代表组合授权,可用于响应中包含的任何
scope
值。 - 组合授权包括用户向 API 项目授予的所有权限范围,即使这些权限是通过不同的客户端请求的也是如此。例如,如果用户使用应用的桌面客户端授予对一个范围的访问权限,然后通过移动客户端向同一应用授予另一个范围的访问权限,则组合授权将包含这两个范围。
- 如果您撤消代表组合授权的令牌,则系统会同时撤消代表关联用户对该授权的所有范围的访问权限。
第 1 步:设置授权参数中的特定于语言的代码示例以及第 2 步:重定向到 Google 的 OAuth 2.0 服务器中的 HTTP/REST 重定向网址示例都使用增量授权。以下代码示例还显示了您需要添加的代码才能使用增量授权。
PHP
$client->setIncludeGrantedScopes(true);
Python
在 Python 中,将 include_granted_scopes
关键字参数设置为 true
,以确保授权请求包含之前授予的范围。include_granted_scopes
很可能不是您设置的唯一关键字实参,如以下示例所示。
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')
Ruby
auth_client.update!( :additional_parameters => {"include_granted_scopes" => "true"} )
Node.js
const authorizationUrl = oauth2Client.generateAuthUrl({ // 'online' (default) or 'offline' (gets refresh_token) access_type: 'offline', /** Pass in the scopes array defined above. * Alternatively, if only one scope is needed, you can pass a scope URL as a string */ scope: scopes, // Enable incremental authorization. Recommended as a best practice. include_granted_scopes: true });
HTTP/REST
GET https://accounts.google.com/o/oauth2/v2/auth? client_id=your_client_id& response_type=code& state=state_parameter_passthrough_value& scope=https%3A//www.googleapis.com/auth/drive.metadata.readonly%20https%3A//www.googleapis.com/auth/calendar.readonly& redirect_uri=https%3A//oauth2.example.com/code& prompt=consent& include_granted_scopes=true
刷新访问令牌(离线访问)
访问令牌会定期过期,并成为相关 API 请求的无效凭据。如果您请求离线访问与令牌关联的范围,则可以刷新访问令牌,而不提示用户授予权限(包括在用户不存在的情况下)。
- 如果您使用 Google API 客户端库,只要您将该对象配置为进行离线访问,客户端对象就会根据需要刷新访问令牌。
- 如果您不使用客户端库,则在将用户重定向到 Google 的 OAuth 2.0 服务器时,需要将
access_type
HTTP 查询参数设置为offline
。在这种情况下,当您使用授权代码换取访问令牌时,Google 的授权服务器会返回刷新令牌。然后,如果访问令牌过期(或在任何其他时间),您可以使用刷新令牌获取新的访问令牌。
在用户离线时,所有需要访问 Google API 的应用都必须请求离线访问。例如,如果应用在预定时间执行备份服务或执行操作,则需要能够在用户不在场时刷新其访问令牌。默认的访问权限样式称为 online
。
服务器端 Web 应用、已安装的应用和设备都会在授权流程中获取刷新令牌。刷新令牌通常不用于客户端 (JavaScript) 网站应用。
PHP
如果您的应用需要对 Google API 进行离线访问,请将 API 客户端的访问类型设置为 offline
:
$client->setAccessType("offline");
用户授予对请求的范围的离线访问权限后,您可以在用户离线时继续使用 API 客户端代表用户访问 Google API。客户端对象会根据需要刷新访问令牌。
Python
在 Python 中,将 access_type
关键字参数设置为 offline
,以确保您能够刷新访问令牌,而无需再次提示用户授予权限。access_type
很可能不是您设置的唯一关键字实参,如以下示例所示。
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')
用户授予对请求的范围的离线访问权限后,您可以在用户离线时继续使用 API 客户端代表用户访问 Google API。客户端对象会根据需要刷新访问令牌。
Ruby
如果您的应用需要对 Google API 进行离线访问,请将 API 客户端的访问类型设置为 offline
:
auth_client.update!( :additional_parameters => {"access_type" => "offline"} )
用户授予对请求的范围的离线访问权限后,您可以在用户离线时继续使用 API 客户端代表用户访问 Google API。客户端对象会根据需要刷新访问令牌。
Node.js
如果您的应用需要对 Google API 进行离线访问,请将 API 客户端的访问类型设置为 offline
:
const authorizationUrl = oauth2Client.generateAuthUrl({ // 'online' (default) or 'offline' (gets refresh_token) access_type: 'offline', /** Pass in the scopes array defined above. * Alternatively, if only one scope is needed, you can pass a scope URL as a string */ scope: scopes, // Enable incremental authorization. Recommended as a best practice. include_granted_scopes: true });
用户授予对请求的范围的离线访问权限后,您可以在用户离线时继续使用 API 客户端代表用户访问 Google API。客户端对象会根据需要刷新访问令牌。
访问令牌会过期。如果访问令牌即将过期,此库会自动使用刷新令牌获取新的访问令牌。如需确保始终存储最新的令牌,您可以使用令牌事件:
oauth2Client.on('tokens', (tokens) => { if (tokens.refresh_token) { // store the refresh_token in your secure persistent database console.log(tokens.refresh_token); } console.log(tokens.access_token); });
此令牌事件仅在首次授权时发生,并且您需要在调用 generateAuthUrl
方法时将 access_type
设置为 offline
才能接收刷新令牌。如果您已向应用授予必要权限,但未设置接收刷新令牌的适当限制,则需要重新授权应用才能接收新的刷新令牌。
如需稍后设置 refresh_token
,您可以使用 setCredentials
方法:
oauth2Client.setCredentials({ refresh_token: `STORED_REFRESH_TOKEN` });
客户端获得刷新令牌后,系统会在下次调用 API 时自动获取和刷新访问令牌。
HTTP/REST
如需刷新访问令牌,您的应用会向 Google 的授权服务器 (https://oauth2.googleapis.com/token
) 发送包含以下参数的 HTTPS POST
请求:
字段 | |
---|---|
client_id |
从 获取的客户端 ID。 |
client_secret |
从 获取的客户端密钥。 |
grant_type |
如 OAuth 2.0 规范中所定义,此字段的值必须设置为 refresh_token 。 |
refresh_token |
从授权代码交换返回的刷新令牌。 |
以下代码段显示了一个示例请求:
POST /token HTTP/1.1 Host: oauth2.googleapis.com Content-Type: application/x-www-form-urlencoded client_id=your_client_id& client_secret=your_client_secret& refresh_token=refresh_token& grant_type=refresh_token
只要用户未撤消向应用授予的访问权限,令牌服务器就会返回包含新访问令牌的 JSON 对象。以下代码段显示了示例响应:
{ "access_token": "1/fFAGRNJru1FTz70BzhT3Zg", "expires_in": 3920, "scope": "https://www.googleapis.com/auth/drive.metadata.readonly https://www.googleapis.com/auth/calendar.readonly", "token_type": "Bearer" }
请注意,系统会对要发出的刷新令牌数量施加限制:每个客户端/用户组合有一个限制,所有客户端中的每个用户还有一个限制。您应将刷新令牌保存在长期存储空间中,并在其有效期间继续使用。如果您的应用请求的刷新令牌过多,可能会遇到这些限制,在这种情况下,较早的刷新令牌将无法使用。
撤消令牌
在某些情况下,用户可能希望撤消向某个应用授予的访问权限。用户可以访问 账号设置来撤消访问权限。如需了解详情,请参阅“有权访问您账号的第三方网站和应用”支持文档中的“撤消网站或应用访问权限”部分。
应用还可以通过程序化方式撤消授予给它的访问权限。 在用户取消订阅、移除应用或应用所需的 API 资源发生重大变化的情况下,程序化撤消非常重要。换句话说,移除流程的一部分可以包含 API 请求,以确保移除之前授予应用的权限。
PHP
如需以编程方式撤消令牌,请调用 revokeToken()
:
$client->revokeToken();
Python
如需以编程方式撤消令牌,请向 https://oauth2.googleapis.com/revoke
发出请求,并将令牌作为参数包含在请求中,并设置 Content-Type
标头:
requests.post('https://oauth2.googleapis.com/revoke', params={'token': credentials.token}, headers = {'content-type': 'application/x-www-form-urlencoded'})
Ruby
如需程序化地撤消令牌,请向 oauth2.revoke
端点发出 HTTP 请求:
uri = URI('https://oauth2.googleapis.com/revoke') response = Net::HTTP.post_form(uri, 'token' => auth_client.access_token)
该令牌可以是访问令牌或刷新令牌。如果令牌是访问令牌且具有相应的刷新令牌,则刷新令牌也会被撤消。
如果撤消成功处理,则响应的状态代码为 200
。对于错误情况,系统会返回状态代码 400
以及错误代码。
Node.js
如需程序化地撤消令牌,请向 /revoke
端点发出 HTTPS POST 请求:
const https = require('https'); // Build the string for the POST request let postData = "token=" + userCredential.access_token; // Options for POST request to Google's OAuth 2.0 server to revoke a token let postOptions = { host: 'oauth2.googleapis.com', port: '443', path: '/revoke', method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(postData) } }; // Set up the request const postReq = https.request(postOptions, function (res) { res.setEncoding('utf8'); res.on('data', d => { console.log('Response: ' + d); }); }); postReq.on('error', error => { console.log(error) }); // Post the request with data postReq.write(postData); postReq.end();
令牌参数可以是访问令牌或刷新令牌。如果令牌是访问令牌且具有相应的刷新令牌,则刷新令牌也会被撤消。
如果撤消成功处理,则响应的状态代码为 200
。对于错误情况,系统会返回状态代码 400
以及错误代码。
HTTP/REST
如需以编程方式撤消令牌,您的应用需要向 https://oauth2.googleapis.com/revoke
发出请求,并将令牌作为参数包含在内:
curl -d -X -POST --header "Content-type:application/x-www-form-urlencoded" \ https://oauth2.googleapis.com/revoke?token={token}
该令牌可以是访问令牌或刷新令牌。如果令牌是访问令牌且具有相应的刷新令牌,则刷新令牌也会被撤消。
如果撤消操作成功处理,则响应的 HTTP 状态代码为 200
。对于错误情况,系统会返回 HTTP 状态代码 400
以及错误代码。
实现跨账号保护
为了保护用户的账号,您还应采取一项额外的措施,即利用 Google 的跨账号保护服务实现跨账号保护。借助此服务,您可以订阅安全事件通知,以便向您的应用提供有关用户账号重大变化的信息。然后,您可以根据自己决定的事件响应方式,使用这些信息来执行操作。
Google 跨账号保护服务向您的应用发送的事件类型示例包括:
-
https://schemas.openid.net/secevent/risc/event-type/sessions-revoked
-
https://schemas.openid.net/secevent/oauth/event-type/token-revoked
-
https://schemas.openid.net/secevent/risc/event-type/account-disabled
如需详细了解如何实现跨账号保护功能以及可用事件的完整列表,请参阅 使用跨账号保护功能保护用户账号 页面。