为网络服务器应用使用 OAuth 2.0

本文档介绍了 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,请按以下步骤操作:

  1. 在 Google API 控制台中打开 API 库。
  2. 如果系统提示您选择一个项目或创建一个新项目,请视需要选择相应选项。
  3. API 库中列出了所有可用的 API(按产品系列和热门程度分组)。 如果列表中没有显示您要启用的 API,请使用搜索功能查找该 API,或点击该 API 所属产品系列中的查看全部。
  4. 选择您要启用的 API,然后点击启用按钮。
  5. 如果系统提示,请启用结算功能。
  6. 如果系统提示您阅读并接受该 API 的服务条款,请按提示操作。

创建授权凭据

任何使用 OAuth 2.0 访问 Google API 的应用都必须具有授权凭据,以便向 Google 的 OAuth 2.0 服务器标识该应用。以下步骤介绍了如何为项目创建凭据。然后,您的应用可以使用这些凭据访问您为该项目启用的 API。

  1. 前往“客户”页面。
  2. 点击创建客户端。
  3. 选择 Web 应用应用类型。
  4. 填写表单,然后点击创建。使用 PHP、Java、Python、Ruby 和 .NET 等语言和框架的应用必须指定已获授权的重定向 URI。重定向 URI 是 OAuth 2.0 服务器可以向其发送响应的端点。这些端点必须遵循 Google 的验证规则。

    为了进行测试,您可以指定引用本地机器的 URI,例如 http://localhost:8080。本文档中的所有示例都使用 http://localhost:8080 作为重定向 URI。

    我们建议您设计应用的身份验证端点,以便应用不会向页面上的其他资源公开授权代码。

创建凭据后,从 API 控制台下载 client_secret.json 文件。将文件安全地存储在只有您的应用可以访问的位置。

确定访问权限范围

借助范围,您的应用可以仅请求访问其所需的资源,同时让用户能够控制他们向您的应用授予的访问权限。因此,请求的范围数量与获得用户同意的可能性之间可能存在反比关系。

在开始实现 OAuth 2.0 授权之前,我们建议您先确定应用需要获得哪些范围的访问权限。

我们还建议您的应用通过增量授权流程请求对授权范围的访问权限,在此流程中,您的应用会请求在上下文中访问用户数据。此最佳实践有助于用户更轻松地了解您的应用为何需要其请求的访问权限。

OAuth 2.0 API 范围文档包含您可能用于访问 Google API 的范围的完整列表。

特定语言的要求

如需运行本文档中的任何代码示例,您需要一个 Google 账号、互联网访问权限和网络浏览器。如果您使用的是某个 API 客户端库,另请参阅以下部分中特定于语言的要求。

PHP

如需运行本文档中的 PHP 代码示例,您需要:

  • PHP 8.0 或更高版本,且已安装命令行界面 (CLI) 和 JSON 扩展程序。
  • 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 云端硬盘和 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 请求。

以下列表简要总结了这些步骤:

  1. 您的应用会标识其所需的权限。
  2. 您的应用会将用户重定向到 Google,并附上请求的权限列表。
  3. 用户决定是否向您的应用授予权限。
  4. 您的应用会了解用户的决定。
  5. 如果用户授予了请求的权限,您的应用会检索代表用户发出 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 云端硬盘元数据和日历活动进行只读离线访问:

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 云端硬盘元数据和日历活动进行只读离线访问:

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 授权服务器支持 Web 服务器应用的以下查询字符串参数:

参数
client_id 必需

应用的客户端 ID。您可以在 Cloud 控制台的“客户端”页面中找到此值。

redirect_uri 必需

确定用户完成授权流程后,API 服务器将用户重定向到的位置。该值必须与您在客户端的 Cloud 控制台“客户端”页面中配置的 OAuth 2.0 客户端的某个已获授权的重定向 URI 完全一致。如果此值与所提供 client_id 的授权重定向 URI 不匹配,您将收到 redirect_uri_mismatch 错误。

请注意,http 或 https 架构、大小写和尾部斜杠(“/”)都必须匹配。

response_type 必需

确定 Google OAuth 2.0 端点是否返回授权代码。

对于 Web 服务器应用,请将参数值设置为 code。

scope 必需

以空格分隔的范围列表,用于标识您的应用可以代表用户访问的资源。这些值会告知 Google 向用户显示的权限请求页面。

借助范围,您的应用可以仅请求访问其所需的资源,同时用户也可以控制向您的应用授予的访问权限。因此,请求的范围数量与获得用户同意的可能性之间存在反比关系。

我们建议您的应用尽可能在上下文中请求对授权范围的访问权限。通过使用增量授权在用户执行相关操作时请求访问用户数据,您可以帮助用户了解您的应用为何需要其请求的访问权限。

access_type 建议

指明您的应用能否在用户不在浏览器前时刷新访问令牌。有效参数值为 online(默认值)和 offline。

如果您的应用需要在用户不在浏览器中时刷新访问令牌,请将该值设置为 offline。这是本文档后面介绍的刷新访问令牌的方法。此值指示 Google 授权服务器在您的应用首次用授权代码换取令牌时返回刷新令牌和访问令牌。

state 建议

指定应用用于在授权请求与授权服务器响应之间保持状态的任何字符串值。 在用户同意或拒绝您应用的访问请求后,服务器会在 redirect_uri 的网址查询组件 (?) 中返回您作为 name=value 对发送的确切值。

您可以将此参数用于多种用途,例如将用户定向到应用中的正确资源、发送 Nonce 以及缓解跨站请求伪造。由于您的 redirect_uri 可能会被猜到,因此使用 state 值可以提高您对传入连接是身份验证请求结果的保证。如果您生成随机字符串或对 Cookie 的哈希值或捕获客户端状态的其他值进行编码,则可以验证响应,以额外确保请求和响应源自同一浏览器,从而防范跨站请求伪造等攻击。如需查看有关如何创建和确认 state 令牌的示例,请参阅 OpenID Connect 文档。

include_granted_scopes 可选

使应用能够使用增量授权来请求访问上下文中的其他范围。如果您将此参数的值设置为 true 且授权请求获得批准,则新的访问令牌还将涵盖用户之前向应用授予访问权限的所有范围。如需查看示例,请参阅增量授权部分。

login_hint 可选

如果您的应用知道哪个用户正在尝试进行身份验证,则可以使用此参数向 Google 身份验证服务器提供提示。服务器会使用该提示来简化登录流程,方法是在登录表单中预填充电子邮件地址字段,或选择适当的多登录会话。

将参数值设置为电子邮件地址或 sub 标识符,这相当于用户的 Google ID。

prompt 可选

以空格分隔、区分大小写的提示列表,用于向用户显示。如果您未指定此参数,则系统仅会在您的项目首次请求访问权限时提示用户。如需了解详情,请参阅 提示用户重新同意。

可能的值包括:

none 不显示任何身份验证或权限请求页面。不得与其他值一起指定。
consent 提示用户表示同意。
select_account 提示用户选择账号。

第 2 步:重定向到 Google 的 OAuth 2.0 服务器

将用户重定向到 Google 的 OAuth 2.0 服务器,以启动身份验证和授权流程。通常,当您的应用首次需要访问用户数据时,就会发生这种情况。在增量授权的情况下,当您的应用首次需要访问其无权访问的其他资源时,也会执行此步骤。

PHP

  1. 生成一个网址,用于向 Google 的 OAuth 2.0 服务器请求访问权限:
    $auth_url = $client->createAuthUrl();
  2. 将用户重定向到 $auth_url:
    header('Location: ' . filter_var($auth_url, FILTER_SANITIZE_URL));

Python

以下示例展示了如何使用 Flask Web 应用框架将用户重定向到授权网址:

return flask.redirect(authorization_url)

Ruby

  1. 生成一个网址,用于向 Google 的 OAuth 2.0 服务器请求访问权限:
    auth_uri = authorizer.get_authorization_url(request: request)
  2. 将用户重定向到 auth_uri。

Node.js

  1. 使用第 1 步 generateAuthUrl 方法中生成的网址 authorizationUrl 向 Google 的 OAuth 2.0 服务器请求访问权限。
  2. 将用户重定向到 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//developers.google.com/oauthplayground&
 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 账号无法授权一个或多个请求的范围。请参阅 Google Workspace 管理员帮助文章 控制哪些第三方应用和内部应用可以访问 Google Workspace 数据,详细了解管理员如何限制对所有范围或敏感和受限范围的访问,直到明确向您的 OAuth 客户端 ID 授予访问权限。

disallowed_useragent

授权端点显示在 Google 的 OAuth 2.0 政策不允许的嵌入式用户代理中。

iOS 和 macOS 开发者在 WKWebView 中打开授权请求时可能会遇到此错误。 开发者应改用 iOS 库,例如 Google 登录 iOS 版或 OpenID Foundation 的 AppAuth iOS 版。

当 iOS 或 macOS 应用在嵌入式用户代理中打开常规网页链接,并且用户从您的网站导航到 Google 的 OAuth 2.0 授权端点时,Web 开发者可能会遇到此错误。开发者应允许在操作系统的默认链接处理程序中打开常规链接,这包括通用链接处理程序或默认浏览器应用。SFSafariViewController 库也是受支持的选项。

org_internal

请求中的 OAuth 客户端 ID 属于一个项目,该项目限制只有特定 Google Cloud 组织中的 Google 账号才能访问。 如需详细了解此配置选项,请参阅“设置 OAuth 权限请求页面”帮助文章中的用户类型部分。

invalid_client

OAuth 客户端密钥不正确。查看 OAuth 客户端配置,包括用于此请求的客户端 ID 和密钥。

deleted_client

用于发出请求的 OAuth 客户端已被删除。删除操作可以手动执行,也可以在客户端未使用 的情况下自动执行。已删除的客户端可在删除后的 30 天内恢复。 了解详情 。

invalid_grant

在刷新访问令牌或使用增量授权时,令牌可能已过期或已失效。 再次对用户进行身份验证,并征求用户同意以获取新令牌。如果您仍然看到此错误,请确保您的应用已正确配置,并且您在请求中使用了正确的令牌和参数。否则,用户账号可能已被删除或停用。

redirect_uri_mismatch

授权请求中传递的 redirect_uri 与 OAuth 客户端 ID 的授权重定向 URI 不匹配。在 Google Cloud 控制台“客户端”页面中查看已获授权的重定向 URI。

redirect_uri 参数可能引用了已弃用且不再受支持的 OAuth 带外 (OOB) 流程。请参阅迁移指南,更新您的集成。

invalid_request

您发出的请求存在问题。这可能是由以下几个原因导致的:

  • 请求格式不正确
  • 请求中缺少必需参数
  • 该请求使用了 Google 不支持的授权方法。验证您的 OAuth 集成是否使用了推荐的集成方法

第 4 步:处理 OAuth 2.0 服务器响应

OAuth 2.0 服务器会使用请求中指定的网址来响应应用的访问请求。

如果用户批准了访问请求,响应中就会包含授权代码。如果用户未批准请求,响应中会包含出错提示。返回到 Web 服务器的授权代码或出错提示会显示在查询字符串中,如以下示例所示:

错误响应:

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//developers.google.com/oauthplayground&
 client_id=client_id

完成 OAuth 2.0 流程后,浏览器会将您重定向到 OAuth 2.0 Playground,这是一个用于测试 OAuth 流程的工具。您会看到 OAuth 2.0 Playground 已自动捕获授权代码。

第 5 步:将授权代码换成刷新令牌和访问令牌

Web 服务器收到授权代码后,便可使用该授权代码换取访问令牌。

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 browser session storage, but for security: client_id, client_secret,
# and token_uri are instead stored only on the backend server.
credentials = flow.credentials
flask.session['credentials'] = {
    'token': credentials.token,
    'refresh_token': credentials.refresh_token,
    '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 从 Cloud 控制台“客户端”页面获取的客户端 ID。
client_secret 可选

从 Cloud 控制台“客户端”页面获取的客户端密钥。

code 初始请求返回的授权代码。
grant_type 根据 OAuth 2.0 规范中的定义,此字段的值必须设置为 authorization_code。
redirect_uri 在 Cloud 控制台“客户端”页面中为给定 client_id 列出的项目重定向 URI 之一。

虽然 DPoP 的使用是可选的,但建议使用它来提高安全性。 DPoP 的安全性依赖于私钥仅限于单个设备;我们建议以无法从设备复制的方式存储私钥,例如使用 TPM、安全隔离区或其他由硬件支持的密钥库。 如需使用 DPoP,您的应用必须为向令牌端点发出的每个请求生成一个新的唯一 DPoP 证明 JWT,并将其添加为 HTTP 请求标头。

标题 必填 说明
DPoP 可选 DPoP 证明是一个 JWT,用于证明对私钥的所有权。这是一个标头,而不是参数。如果提供,则返回的令牌会绑定到此密钥。必须为每个请求生成一个新的唯一证明,并且必须包含与请求匹配的 htm(HTTP 方法)和 htu (HTTP URI) 声明。

以下代码段显示了一个示例请求:

POST /token HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6Ik\
 VDIiwieCI6Imw4dEZyaHgtMzR0VjNoUklDUkRZOXpDa0RscEJoRjQyVVFVZldWQVdCR\
 nMiLCJ5IjoiOVZFNGpmX09rX282NHpiVFRsY3VOSmFqSG10NnY5VERWclUwQ2R2R1JE\
 QSIsImNydiI6IlAtMjU2In19.eyJqdGkiOiItQndDM0VTYzZhY2MybFRjIiwiaHRtIj\
 oiUE9TVCIsImh0dSI6Imh0dHBzOi8vc2VydmVyLmV4YW1wbGUuY29tL3Rva2VuIiwia\
 WF0IjoxNTYyMjYyNjE2fQ.2-GxA6T8lP4vfrg8v-FdWP0A0zdrj8igiMLvqRMUvwnQg\
 4PtFLbdLXiOSsX0x7NVY-FNyJK70nfbV37xRZT3Lg

code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7&
client_id=your_client_id&
redirect_uri=https%3A//developers.google.com/oauthplayground&
grant_type=authorization_code

构建 DPoP 证明

以下步骤展示了如何通过命令行使用 OpenSSL 构建 DPoP 证明:

  1. 生成 EC P-256 密钥对:
    openssl ecparam -name prime256v1 -genkey -noout -out dpop_private.pem
    openssl ec -in dpop_private.pem -pubout -out dpop_public.pem
  2. 创建 DPoP 标头:

    标头必须包含 typ、alg 和 jwk(公钥)声明。x 和 y 值是 EC 公钥的 Base64Url 编码坐标。 对以下 JSON 进行 Base64Url 编码:

    {
      "typ":"dpop+jwt",
      "alg":"ES256",
      "jwk": {
        "kty":"EC",
        "x":"YOUR_PUBLIC_KEY_X",
        "y":"YOUR_PUBLIC_KEY_Y",
        "crv":"P-256"
      }
    }
  3. 创建 DPoP 载荷:

    载荷必须包含 jti(请求的唯一标识符)、htm(HTTP 方法,例如 POST)、htu(HTTP URI,例如 https://oauth2.googleapis.com/token)和 iat(签发时间)。如果您在之前请求的响应中通过 DPoP-Nonce 标头从服务器收到了 Nonce,则必须在 nonce 声明中包含该 Nonce 值。nonce 声明对于授权代码交换是可选的,仅在之前收到 DPoP-Nonce 标头时使用。 对以下 JSON 进行 Base64Url 编码:

    {
      "jti":"JTI_VALUE",
      "htm":"POST",
      "htu":"https://oauth2.googleapis.com/token",
      "iat":YOUR_JWT_ISSUED_TIME,
      "nonce":"SERVER_PROVIDED_NONCE"
    }

    jti 值取决于广告交易平台类型:

    • 对于授权代码交换,jti 必须是授权代码的 Base64Url 编码 SHA256 哈希:"jti":"BASE64URL(SHA256(AUTHORIZATION_CODE))"。
    • 对于刷新令牌交换,jti 必须是每个请求的唯一标识符:"jti":"YOUR_UNIQUE_PER_REQUEST_IDENTIFIER"。
  4. 对证明进行签名:

    使用英文句点 (.) 拼接编码后的标头和载荷,然后使用 ES256 算法和您的私钥对结果进行签名。请注意,JWS 要求签名采用原始 R | S 串联格式(P-256 为 64 字节)。如果直接使用 OpenSSL,则必须将默认的 ASN.1 DER 编码签名转换为此原始格式。

200 OK 响应包含令牌,表示交换成功。如果在交换期间使用了有效的 DPoP 证明,Google 返回的刷新令牌将与您的密钥进行 DPoP 绑定,但访问令牌不会进行 DPoP 绑定。访问令牌将保留 Bearer 的 token_type,而不是 DPoP。此外,Google 会在响应中返回 DPoP-Nonce HTTP 标头。 您的客户端必须缓存此 Nonce,并将其包含在后续请求(例如,用刷新令牌换取新的访问令牌时,或调用受 DPoP 保护的 API 时)中 DPoP 证明的 nonce 声明中。通过使用此提前发放的 Nonce,您可以避免在下次请求时出现额外的往返失败 (use_dpop_nonce)。

对于使用 DPoP 绑定刷新令牌发出的令牌交换请求,必须包含 DPoP 证明。

如果 DPoP 标头在预期时缺失、无效,或者证明使用的密钥与绑定到令牌的密钥不同,则会发生交换失败。在这些情况下,服务器会返回 400 Bad Request 错误。 如果 DPoP 证明具有不匹配的 htm 或 htu 声明、过期的 iat、重复使用的 jti 或无效签名,Google 会返回 invalid_dpop_proof 错误代码。 如果需要 DPoP 随机数(例如在刷新令牌交换期间),并且 DPoP 证明缺少 nonce 声明,或者随机数值不被服务器接受(例如,它已过期、已被使用或不正确),Google 会返回 use_dpop_nonce 错误代码以及 DPoP-Nonce HTTP 标头,其中包含您可以在后续请求中使用的新随机数。 其他失败情况可能会返回 invalid_grant。

Google 会返回一个 JSON 对象来响应此请求,该对象包含一个短期访问令牌和一个刷新令牌。 请注意,只有当您的应用在向 Google 授权服务器发出的初始请求中将 access_type 参数设置为 offline 时,系统才会返回刷新令牌。

响应包含以下字段:

字段
access_token 您的应用为授权 Google API 请求而发送的令牌。
expires_in 访问令牌的剩余生命周期(以秒为单位)。
refresh_token 可用于获取新访问令牌的令牌。刷新令牌在用户撤消访问权限或刷新令牌过期之前一直有效。如果使用了 DPoP,刷新令牌会绑定到用于为 DPoP 证明签名的私钥。 同样,只有当您在向 Google 授权服务器发出的初始请求中将 access_type 参数设置为 offline 时,此字段才会出现在此响应中。
refresh_token_expires_in 刷新令牌的剩余生命周期(以秒为单位)。仅当用户授予基于时间的访问权限时,系统才会设置此值。
scope access_token 授予的访问权限范围,表示为以空格分隔、区分大小写的字符串列表。
token_type 返回的令牌类型。此值始终为 Bearer,即使使用 DPoP 也是如此。

以下代码段展示了使用 DPoP 时成功的响应标头和正文示例:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
DPoP-Nonce: AN3XwJjZsjnb0ZuWkRlek8QU7wY-Zhf-5IP6tO0tORz0KgtDT1Bo8FX-w4nz3r5lnepI

{
  "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 步:检查用户授予了哪些范围

请求多项权限(范围)时,用户可能不会向您的应用授予所有权限。您的应用必须验证实际授予了哪些范围,并妥善处理某些权限被拒的情况,通常是停用依赖于这些被拒范围的功能。

但也有例外情况。具有全网域授权的 Google Workspace 企业版应用或标记为受信任的应用会绕过精细权限权限请求页面。对于这些应用,用户不会看到精细权限请求页面。相反,您的应用要么会获得所有请求的范围,要么一个也得不到。

如需了解详情,请参阅如何处理精细权限。

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,
    '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:

  1. 如果您需要将访问令牌应用于新的 Google\Client 对象(例如,如果您将访问令牌存储在用户会话中),请使用 setAccessToken 方法:
    $client->setAccessToken($access_token);
  2. 为要调用的 API 构建服务对象。您可以通过向要调用的 API 的构造函数提供已获授权的 Google\Client 对象来构建服务对象。 例如,如需调用 Drive API,请执行以下操作:
    $drive = new Google\Service\Drive($client);
  3. 使用 服务对象提供的接口向 API 服务发出请求。 例如,如需列出经过身份验证的用户 Google 云端硬盘中的文件,请执行以下操作:
    $files = $drive->files->listFiles(array());

Python

获取访问令牌后,您的应用可以使用该令牌代表给定用户账号或服务账号对 API 请求进行授权。使用用户专属授权凭证为您要调用的 API 构建服务对象,然后使用该对象发出已获授权的 API 请求。

  1. 为要调用的 API 构建服务对象。您可以通过使用 API 的名称和版本以及用户凭据调用 googleapiclient.discovery 库的 build 方法来构建服务对象: 例如,如需调用 Drive API 的第 3 版,请执行以下操作:
    from googleapiclient.discovery import build
    
    drive = build('drive', 'v2', credentials=credentials)
  2. 使用服务对象提供的接口向 API 服务发出请求。 例如,如需列出经过身份验证的用户 Google 云端硬盘中的文件,请执行以下操作:
    files = drive.files().list().execute()

Ruby

获取访问令牌后,您的应用可以使用该令牌代表指定的用户账号或服务账号发出 API 请求。使用用户专属授权凭证为您要调用的 API 构建服务对象,然后使用该对象发出已获授权的 API 请求。

  1. 为您要调用的 API 构建服务对象。 例如,如需调用 Drive API 的第 3 版,请使用以下代码:
    drive = Google::Apis::DriveV3::DriveService.new
  2. 在服务上设置凭据:
    drive.authorization = credentials
  3. 使用服务对象提供的接口向 API 服务发出请求。 例如,如需列出经过身份验证的用户 Google 云端硬盘中的文件,请执行以下操作:
    files = drive.list_files

或者,您也可以通过向方法提供 options 参数,按方法提供授权:

files = drive.list_files(options: { authorization: credentials })

Node.js

获取访问令牌并将其设置到 OAuth2 对象后,使用该对象调用 Google API。您的应用可以使用该令牌代表给定用户账号或服务账号为 API 请求授权。为您要调用的 API 构建服务对象。 例如,以下代码使用 Google Drive 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。为此,请在向 API 发出的请求中添加访问令牌,方法是添加 access_token 查询参数或 Authorization HTTP 标头 Bearer 值。如果可能,最好使用 HTTP 标头,因为查询字符串往往会在服务器日志中显示。在大多数情况下,您可以使用客户端库来设置对 Google API 的调用(例如,调用 Drive Files API 时)。

您可以在 OAuth 2.0 Playground 中试用所有 Google API 并查看其范围。

HTTP GET 示例

使用 Authorization: Bearer HTTP 标头对 drive.files 端点(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

完整示例

以下示例会在用户进行身份验证并同意应用访问其云端硬盘元数据后,输出用户 Google 云端硬盘中文件的 JSON 格式列表。

PHP

如需运行此示例,请执行以下操作:

  1. 在 API 控制台中,将本地机器的网址添加到重定向网址列表中。例如,添加 http://localhost:8080。
  2. 创建一个新目录并切换到该目录。例如:
    mkdir ~/php-oauth2-example
    cd ~/php-oauth2-example
  3. 使用 Composer 安装 PHP 版 Google API 客户端库:
    composer require google/apiclient:^2.15.0
  4. 创建包含以下内容的 index.php 和 oauth2callback.php 文件。
  5. 使用 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 响应。
  • 用于调用 Calendar API 的模拟页面:此链接指向一个模拟页面,如果用户授予了权限,该页面会尝试执行示例 Calendar API 请求。如有必要,它会启动授权流程。如果成功,页面会显示 API 响应。
  • 直接测试授权流程:此链接指向一个尝试引导用户完成授权流程的页面。应用请求获得代表用户提交已获授权的 API 请求的权限。
  • 撤消当前凭据:此链接指向一个页面,该页面会 撤消用户已授予应用的权限。
  • 清除 Flask 会话凭据:此链接会清除存储在 Flask 会话中的授权凭据。这样,您就可以了解如果已向您的应用授予权限的用户尝试在新会话中执行 API 请求,会发生什么情况。此外,您还可以查看以下情况下的 API 响应:用户撤消了授予您应用的权限,但您的应用仍尝试使用已撤消的访问令牌对请求进行授权。
# -*- coding: utf-8 -*-

import os
import flask
import json
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 client secrets from the server-side file.
    with open(CLIENT_SECRETS_FILE, 'r') as f:
        client_config = json.load(f)['web']

    # Load user-specific credentials from browser session storage.
    session_credentials = flask.session['credentials']

    # Reconstruct the credentials object.
    credentials = google.oauth2.credentials.Credentials(
        refresh_token=session_credentials.get('refresh_token'),
        scopes=session_credentials.get('granted_scopes'),
        token=session_credentials.get('token'),
        client_id=client_config.get('client_id'),
        client_secret=client_config.get('client_secret'),
        token_uri=client_config.get('token_uri'))

    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.
    flask.session['credentials'] = credentials_to_dict(credentials)

    return flask.jsonify(**files)
  else:
    # User didn't authorize read-only Drive activity permission.
    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.')

  # Load client secrets from the server-side file.
  with open(CLIENT_SECRETS_FILE, 'r') as f:
      client_config = json.load(f)['web']

  # Load user-specific credentials from the session.
  session_credentials = flask.session['credentials']

  # Reconstruct the credentials object.
  credentials = google.oauth2.credentials.Credentials(
      refresh_token=session_credentials.get('refresh_token'),
      scopes=session_credentials.get('granted_scopes'),
      token=session_credentials.get('token'),
      client_id=client_config.get('client_id'),
      client_secret=client_config.get('client_secret'),
      token_uri=client_config.get('token_uri'))

  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:
    # Clear the user's session credentials after successful revocation
    if 'credentials' in flask.session:
        del flask.session['credentials']
        del flask.session['features']
    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,
          '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="/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="/drive">Call Drive API directly</a></td>' +
          '<td> Use stored credentials to call the API, you still might not be prompted to reauthorize ' +
          '    the application.</td></tr>' +
          '<tr><td><a href="/calendar">Call Calendar API directly</a></td>' +
          '<td> Use stored credentials to call the API, 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="/authorize">authorize</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 Google API Console.
  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

如需运行此示例,请执行以下操作:

  1. 在 API 控制台中,将本地机器的网址添加到重定向网址列表中。例如,添加 http://localhost。
  2. 确保您已安装维护 LTS、活跃 LTS 或当前版本的 Node.js。
  3. 创建一个新目录并切换到该目录。例如:
    mkdir ~/nodejs-oauth2-example
    cd ~/nodejs-oauth2-example
  4. 使用 npm 安装适用于 Node.js 的 Google API 客户端库:
    npm install googleapis
  5. 创建包含以下内容的文件 main.js。
  6. 运行示例:
    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 节。

验证规则
Scheme

重定向 URI 必须使用 HTTPS 架构,而不是纯 HTTP。Localhost URI(包括 localhost IP 地址 URI)不受此规则约束。

主机

主机不能是原始 IP 地址。本地主机 IP 地址不受此规则约束。

网域
  • 主机 TLD(顶级域名)必须属于公共后缀列表。
  • 主机网域不能是 “googleusercontent.com”。
  • 重定向 URI 不能包含网址缩短服务网域(例如 goo.gl),除非应用拥有该网域。此外,如果拥有短网址域名的应用选择重定向到该域名,则该重定向 URI 的路径中必须包含 “/google-callback/” 或以 “/google-callback” 结尾。
  • Userinfo

    重定向 URI 不能包含 userinfo 子组件。

    路径

    重定向 URI 不能包含路径遍历(也称为目录回溯),路径遍历由 “/..” 或 “\..” 或其网址编码表示。

    查询

    重定向 URI 不能包含开放式重定向。

    fragment

    重定向 URI 不能包含片段组件。

    字符数 重定向 URI 不能包含某些字符,包括:
    • 通配符 ('*')
    • 不可打印的 ASCII 字符
    • 百分号编码无效(任何不遵循网址编码形式的百分号编码,即百分号后跟两个十六进制数字)
    • 空字符(编码的 NULL 字符,例如 %00、%C0%80)

    增量授权

    在 OAuth 2.0 协议中,您的应用会请求访问资源的授权,这些资源由范围标识。最佳用户体验做法是在需要时请求资源授权。为了实现这种做法,Google 的授权服务器支持增量授权。借助此功能,您可以根据需要请求范围,如果用户授予新范围的权限,则会返回一个授权代码,该代码可用于换取包含用户已授予项目的所有范围的令牌。

    例如,如果一款应用可让用户试听音乐曲目并创建混音,那么在登录时可能只需要极少的资源,或许只需要登录用户的姓名。不过,保存完成的混音需要访问其 Google 云端硬盘。大多数用户会认为,只有在应用实际需要访问其 Google 云端硬盘时才请求访问权限,这才是自然的做法。

    在这种情况下,应用可能会在登录时请求 openid 和 profile 范围来执行基本登录,然后在首次请求保存混音时请求 https://www.googleapis.com/auth/drive.file 范围。

    如需实现增量授权,您需要完成请求访问令牌的正常流程,但要确保授权请求包含之前授予的范围。此方法可让您的应用避免管理多个访问令牌。

    以下规则适用于通过增量授权获得的访问令牌:

    • 该令牌可用于访问与新合并授权中包含的任何范围相对应的资源。
    • 当您使用组合授权的刷新令牌获取访问令牌时,该访问令牌代表组合授权,可用于响应中包含的任何 scope 值。
    • 合并后的授权包括用户向 API 项目授予的所有范围,即使这些授权是从不同的客户端请求的。例如,如果用户使用应用的桌面客户端向一个范围授予了访问权限,然后通过移动客户端向同一应用授予了另一个范围,则合并后的授权将包含这两个范围。
    • 如果您撤消代表组合授权的令牌,则代表关联用户对该授权的所有范围的访问权限也会同时撤消。

    第 1 步:设置授权重定向到 Google 的 OAuth 2.0 服务器中的语言专用代码示例均使用增量授权。以下代码示例还展示了您需要添加哪些代码才能使用增量授权。

    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//developers.google.com/oauthplayground&
      prompt=consent&
      include_granted_scopes=true

    刷新访问令牌(离线访问)

    访问令牌会定期过期,并成为相关 API 请求的无效凭证。如果您请求了对与令牌关联的范围的离线访问权限,则可以刷新访问令牌,而无需提示用户授予权限(包括在用户不在场时)。

    • 如果您使用 Google API 客户端库,只要您将客户端对象配置为离线访问,该对象就会根据需要刷新访问令牌。
    • 如果您未使用客户端库,则需要在将用户重定向到 Google 的 OAuth 2.0 服务器时,将 access_type HTTP 查询参数设置为 offline。在这种情况下,当您用授权代码换取访问令牌时,Google 的授权服务器会返回刷新令牌。然后,如果访问令牌过期(或在任何其他时间),您可以使用刷新令牌获取新的访问令牌。

    对于任何需要在用户不在场时访问 Google API 的应用,都必须请求离线访问权限。例如,执行备份服务或在预定时间执行操作的应用需要在用户不在场时刷新其访问令牌。默认访问权限样式称为 online。

    服务器端 Web 应用、已安装的应用和设备都会在授权过程中获取刷新令牌。刷新令牌通常不用于客户端 (JavaScript) Web 应用。

    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 从 API 控制台获取的客户端 ID。
    client_secret 可选

    从 API 控制台获取的客户端密钥。

    grant_type 如 OAuth 2.0 规范中所定义,此字段的值必须设置为 refresh_token。
    refresh_token 从授权代码交换返回的刷新令牌。

    虽然 DPoP 的使用是可选的,但建议使用它来提高安全性。 如需将 DPoP 与刷新令牌搭配使用,您的应用必须为向令牌端点发出的每个请求生成一个新的唯一 DPoP 证明 JWT。此证明必须使用在初始授权代码交换期间使用的同一私钥进行签名。您的应用会将证明添加为 HTTP 请求标头。

    标题 必填 说明
    DPoP 可选 DPoP 证明是一个 JWT,用于证明对私钥的所有权。这是一个标头,而不是参数。如果提供,则返回的令牌会绑定到此密钥。必须为每个请求生成一个新的唯一证明,并且必须包含与请求匹配的 htm(HTTP 方法)和 htu (HTTP URI) 声明。

    以下代码段显示了一个示例请求:

    POST /token HTTP/1.1
    Host: oauth2.googleapis.com
    Content-Type: application/x-www-form-urlencoded
    DPoP: DPOP_PROOF_JWT
    
    client_id=your_client_id&
    refresh_token=refresh_token&
    grant_type=refresh_token

    如需将 DPoP 与刷新令牌搭配使用,您必须为请求生成一个新的唯一 DPoP 证明 JWT。如需了解如何生成密钥对和构建 JWT,请参阅构建 DPoP 证明中的分步说明。

    成功交换后,系统会返回 200 OK 响应,其中包含新的访问令牌。使用 DPoP 时,token_type 为 Bearer。成功的响应会确认刷新令牌的 DPoP 证明已被接受。Google 还可能会在响应中返回新的 DPoP-Nonce HTTP 标头;如果返回,您的客户端必须缓存此 Nonce,并将其包含在后续请求中 DPoP 证明的 nonce 声明中。

    如果 DPoP 标头缺失、无效或使用了错误的密钥,则会发生交换失败。如需详细了解特定 DPoP 错误代码和处理 Nonce,请参阅交换失败。

    以下代码段展示了使用 DPoP 时成功的响应标头和正文示例:

    HTTP/1.1 200 OK
    Content-Type: application/json; charset=utf-8
    DPoP-Nonce: AN3XwJjZsjnb0ZuWkRlek8QU7wY-Zhf-5IP6tO0tORz0KgtDT1Bo8FX-w4nz3r5lnepI
    
    {
      "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 产品中,用户可以在意见征求流程中选择基于时间的访问权限,从而授予在限定时间内访问数据的权限。例如, Data Portability API 可实现一次性数据传输。

    当用户向您的应用授予基于时间的访问权限时,刷新令牌将在指定时长后过期。请注意,在特定情况下,刷新令牌可能会提前失效;如需了解详情,请参阅这些情况。在这种情况下,授权代码交换响应中返回的 refresh_token_expires_in 字段表示刷新令牌过期前的剩余时间。

    实现跨账号保护

    为了保护用户账号,您还应采取一项额外措施,即利用 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

    如需详细了解如何实现跨账号保护,以及查看可用事件的完整列表,请参阅 使用跨账号保护功能保护用户账号 页面。