为服务器到服务器应用使用 OAuth 2.0

Google OAuth 2.0 系统支持服务器到服务器的互动,例如网络应用和 Google 服务之间的服务器到服务器互动。在这种情况下,您需要使用服务账号,该账号属于您的应用,而不是某个最终用户。您的应用会代表服务账号调用 Google API,因此用户不会直接参与。这种情形有时称为“双腿 OAuth”或“2LO”。(相关术语“三方模式 OAuth”是指您的应用代表最终用户调用 Google API 的场景,有时需要征得用户同意。)

通常,当应用使用 Google API 来处理自己的数据(而非用户的数据)时,会使用服务账号。例如,使用 Google Cloud Datastore 进行数据持久化的应用会使用服务账号来验证其对 Google Cloud Datastore API 的调用。

Google Workspace 网域管理员还可以向服务账号授予全网域权限,以代表网域中的用户访问用户数据。

本文档介绍了应用如何使用 Google API 客户端库(推荐)或 HTTP 完成服务器到服务器的 OAuth 2.0 流程。

概览

如需支持服务器到服务器的交互,请先在 API Console中为您的项目创建服务账号。如果您想访问 Google Workspace 账号中用户的数据,请向服务账号委派全网域访问权限。

然后,您的应用准备使用服务账号的凭据从 OAuth 2.0 授权服务器请求访问令牌,以执行已获授权的 API 调用。

最后,您的应用可以使用访问令牌来调用 Google API。

创建服务账号

服务账号的凭据包括一个生成的唯一电子邮件地址和至少一个公钥/私钥对。如果启用了全网域授权,则客户端 ID 也是服务账号凭据的一部分。

如果您的应用在 Google App Engine 上运行,则在您创建项目时,系统会自动设置服务账号。

如果您的应用在 Google Compute Engine 上运行,系统也会在您创建项目时自动设置服务账号,但您必须在创建 Google Compute Engine 实例时指定应用需要访问的范围。如需了解详情,请参阅准备实例以使用服务账号

如果您的应用不在 Google App Engine 或 Google Compute Engine 上运行,您必须在 Google API Console中获取这些凭据。如需生成服务账号凭据或查看您已生成的公共凭据,请执行以下操作:

首先,创建一个服务帐户:

  1. 打开 Service accounts page
  2. If prompted, select a project, or create a new one.
  3. 单击创建服务帐户
  4. Service account details下,键入服务帐户的名称、ID 和描述,然后点击Create and continue
  5. 可选:在Grant this service account access to project下,选择要授予服务帐户的 IAM 角色。
  6. 单击继续
  7. 可选:在Grant users access to this service account下,添加允许使用和管理服务帐户的用户或组。
  8. 单击完成

接下来,创建一个服务帐户密钥:

  1. 单击您创建的服务帐户的电子邮件地址。
  2. 单击密钥选项卡。
  3. 添加密钥下拉列表中,选择创建新密钥
  4. 单击创建

您的新公钥/私钥对已生成并下载到您的机器上;它作为私钥的唯一副本。您有责任安全地存储它。如果您丢失了这个密钥对,您将需要生成一个新的。

您可以随时返回 API Console,查看电子邮件地址、公钥指纹和其他信息,或生成其他公钥/私钥对。如需详细了解 API Console中的服务账号凭据,请参阅 API Console帮助文件中的服务账号

记下服务账号的电子邮件地址,并将服务账号的私钥文件存储在您的应用可以访问的位置。您的应用需要这些凭据才能进行已获授权的 API 调用。

向服务账号进行全网域授权

组织中的 Workspace 管理员可以使用 Google Workspace 账号授权应用代表 Google Workspace 网域中的用户访问 Workspace 用户数据。例如,如果一个应用使用 Google Calendar API 向 Google Workspace 网域中所有用户的日历添加活动,则该应用会使用服务账号代表用户访问 Google Calendar API。有时候,授权服务账号代表网域中的用户访问数据被称为向服务账号进行“全网域授权”。

如需向服务账号委派全网域权限,Google Workspace 网域的超级管理员必须完成以下步骤:

  1. 在 Google Workspace 网域的 管理控制台中,依次前往主菜单 > 安全性 > 访问权限和数据控件 > API 控件
  2. 全网域授权窗格中,选择管理全网域授权
  3. 点击新增
  4. 客户端 ID 字段中,输入服务账号的客户端 ID。您可以在 Service accounts page中找到服务账号的客户端 ID。
  5. OAuth 范围(以英文逗号分隔)字段中,输入您的应用应被授予访问权限的范围列表。例如,如果您的应用需要对 Google Drive API 和 Google Calendar API 拥有网域级完全访问权限,请输入:https://www.googleapis.com/auth/drive, https://www.googleapis.com/auth/calendar
  6. 点击授权

现在,您的应用有权以 Workspace 网域中用户的身份进行 API 调用(即“模拟”用户)。当您准备进行这些委托的 API 调用时,您将明确指定要模拟的用户。

准备进行委托的 API 调用

Java

从 API Console中获取客户端电子邮件地址和私钥后,使用 Google Auth Library for Java 从服务账号的凭据和应用需要访问的范围创建 GoogleCredentials 对象。例如:

import com.google.auth.oauth2.GoogleCredentials;
import com.google.api.services.sqladmin.SQLAdminScopes;

// ...

GoogleCredentials credentials = GoogleCredentials.fromStream(new FileInputStream("MyProject-1234.json"))
    .createScoped(Collections.singleton(SQLAdminScopes.SQLSERVICE_ADMIN));

如果您是在 Google Cloud Platform 上开发应用,则可以改用应用默认凭据,从而简化该流程。

进行全网域授权

如果您已向服务账号授予全网域访问权限,并且想要模拟用户账号,请使用 GoogleCredentials 对象的 createDelegated 方法指定用户账号的电子邮件地址。例如:

GoogleCredentials credentials = GoogleCredentials.fromStream(new FileInputStream("MyProject-1234.json"))
    .createScoped(Collections.singleton(SQLAdminScopes.SQLSERVICE_ADMIN))
    .createDelegated("workspace-user@example.com");

上述代码使用 GoogleCredentials 对象调用其 createDelegated() 方法。createDelegated() 方法的实参必须是属于您的 Workspace 账号的用户。发出请求的代码将使用此凭据通过您的服务账号调用 Google API。

Python

从 API Console获取客户端电子邮件地址和私钥后,使用 Google API Python 客户端库完成以下步骤:

  1. 根据服务账号的凭据和应用需要访问的范围创建 Credentials 对象。例如:
    from google.oauth2 import service_account
    
    SCOPES = ['https://www.googleapis.com/auth/sqlservice.admin']
    SERVICE_ACCOUNT_FILE = '/path/to/service.json'
    
    credentials = service_account.Credentials.from_service_account_file(
            SERVICE_ACCOUNT_FILE, scopes=SCOPES)

    如果您是在 Google Cloud Platform 上开发应用,则可以改用应用默认凭据,从而简化该流程。

  2. 进行全网域授权

    如果您已向服务账号委派全网域访问权限,并且想要模拟用户账号,请使用现有 ServiceAccountCredentials 对象的 with_subject 方法。例如:

    delegated_credentials = credentials.with_subject('user@example.org')

使用 Credentials 对象在应用中调用 Google API。

HTTP/REST

从 API Console获取客户端 ID 和私钥后,您的应用需要完成以下步骤:

  1. 创建 JSON Web 令牌(JWT,发音为“jot”),其中包含标头、声明集和签名。
  2. 从 Google OAuth 2.0 授权服务器请求访问令牌。
  3. 处理授权服务器返回的 JSON 响应。

以下部分介绍了如何完成这些步骤。

如果响应包含访问令牌,您可以使用该访问令牌调用 Google API。(如果响应不包含访问令牌,则可能是您的 JWT 和令牌请求格式不正确,或者服务账号没有访问所请求范围的权限。)

当访问令牌过期时,您的应用会生成另一个 JWT 并对其进行签名,然后请求另一个访问令牌。

您的服务器应用使用 JWT 从 Google 授权服务器请求令牌,然后使用该令牌调用 Google API 端点。不涉及任何最终用户。

本部分其余内容将详细介绍如何创建 JWT、对 JWT 进行签名、构建访问令牌请求以及处理响应。

创建 JWT

JWT 由三部分组成:标头、声明集和签名。标头和声明集是 JSON 对象。这些 JSON 对象会序列化为 UTF-8 字节,然后使用 Base64url 编码进行编码。此编码可针对因重复编码操作而导致的编码更改提供恢复能力。标头、声明集和签名使用句点 (.) 字符串联在一起。

JWT 的组成如下:

{Base64url encoded header}.{Base64url encoded claim set}.{Base64url encoded signature}

签名的基本字符串如下:

{Base64url encoded header}.{Base64url encoded claim set}
构成 JWT 标头

标头包含三个字段,分别用于指明签名算法、断言的格式以及用于签署 JWT 的 [服务账号密钥的密钥 ID](https://cloud.google.com/iam/docs/reference/rest/v1/projects.serviceAccounts.keys)。算法和格式是必需的,并且每个字段只有一个值。随着更多算法和格式的推出,此标头也会相应变化。密钥 ID 是可选的,如果指定的密钥 ID 不正确,GCP 将尝试使用与服务账号关联的所有密钥来验证令牌,如果找不到有效的密钥,则会拒绝该令牌。Google 保留将来拒绝具有错误密钥 ID 的令牌的权利。

服务账号依赖于 RSA SHA-256 算法和 JWT 令牌格式。因此,标头的 JSON 表示法如下所示:

{"alg":"RS256","typ":"JWT", "kid":"370ab79b4513eb9bad7c9bd16a95cb76b5b2a56a"}

其 Base64url 表示形式如下:

          eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsICJraWQiOiIzNzBhYjc5YjQ1MTNlYjliYWQ3YzliZDE2YTk1Y2I3NmI1YjJhNTZhIn0=
构成 JWT 声明集

JWT 声明集包含有关 JWT 的信息,包括所请求的权限(范围)、令牌的目标对象、签发者、令牌的签发时间以及令牌的有效期。大多数字段都是必填字段。与 JWT 标头类似,JWT 声明集也是一个 JSON 对象,用于计算签名。

必需的声明

JWT 声明集中所需的声明如下所示。它们可以按任意顺序出现在声明集中。

名称 说明
iss 服务账号的电子邮件地址。
scope 以空格分隔的应用所请求的权限列表。
aud 断言的预期目标描述符。在发出访问令牌请求时,此值始终为 https://oauth2.googleapis.com/token
exp 断言的到期时间,以 1970 年 1 月 1 日 00:00:00 UTC 以来的秒数指定。此值的有效期最长为签发时间后 1 小时。
iat 断言的颁发时间,以 1970 年 1 月 1 日 00:00:00 UTC 以来的秒数指定。

JWT 声明集中必需字段的 JSON 表示法如下所示:

{
  "iss": "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com",
  "scope": "https://www.googleapis.com/auth/devstorage.read_only",
  "aud": "https://oauth2.googleapis.com/token",
  "exp": 1328554385,
  "iat": 1328550785
}
其他声明

在某些企业用例中,应用可以使用全网域授权代表组织中的特定用户执行操作。必须先授予执行此类模拟操作的权限,应用才能模拟用户,而此权限通常由超级管理员处理。如需了解详情,请参阅使用全网域授权功能控制 API 访问权限

如需获取授予应用对资源委托访问权限的访问令牌,请在 JWT 声明集中包含用户的电子邮件地址作为 sub 字段的值。

名称 说明
sub 应用正在请求委托访问权限的用户的电子邮件地址。

如果应用没有模拟用户的权限,则包含 sub 字段的访问令牌请求的响应将为 错误

下面展示了一个包含 sub 字段的 JWT 声明集示例:

{
  "iss": "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com",
  "sub": "some.user@example.com",
  "scope": "https://www.googleapis.com/auth/prediction",
  "aud": "https://oauth2.googleapis.com/token",
  "exp": 1328554385,
  "iat": 1328550785
}
对 JWT 声明集进行编码

与 JWT 标头一样,JWT 声明集应序列化为 UTF-8 并进行 Base64url 安全编码。以下是 JWT 声明集的 JSON 表示法示例:

{
  "iss": "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com",
  "scope": "https://www.googleapis.com/auth/prediction",
  "aud": "https://oauth2.googleapis.com/token",
  "exp": 1328554385,
  "iat": 1328550785
}
计算签名

JSON Web 签名 (JWS) 是一项规范,用于指导 JWT 签名生成机制。签名的输入是以下内容的字节数组:

{Base64url encoded header}.{Base64url encoded claim set}

计算签名时必须使用 JWT 标头中的签名算法。Google OAuth 2.0 授权服务器支持的唯一签名算法是使用 SHA-256 哈希算法的 RSA。这在 JWT 标头的 alg 字段中表示为 RS256

使用从 Google API Console获取的私钥,通过 SHA256withRSA(也称为 RSASSA-PKCS1-V1_5-SIGN,使用 SHA-256 哈希函数)对输入的 UTF-8 表示法进行签名。输出将是一个字节数组。

然后,必须对签名进行 Base64url 编码。标头、声明集和签名使用句点 (.) 字符串联在一起。结果是 JWT。它应为以下内容(为清晰起见,添加了换行符):

{Base64url encoded header}.
{Base64url encoded claim set}.
{Base64url encoded signature}

以下是 Base64url 编码之前的 JWT 示例:

{"alg":"RS256","typ":"JWT"}.
{
"iss":"761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com",
"scope":"https://www.googleapis.com/auth/prediction",
"aud":"https://oauth2.googleapis.com/token",
"exp":1328554385,
"iat":1328550785
}.
[signature bytes]

以下是已签名并可供传输的 JWT 的示例:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvcHJlZGljdGlvbiIsImF1ZCI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL29hdXRoMi92NC90b2tlbiIsImV4cCI6MTMyODU1NDM4NSwiaWF0IjoxMzI4NTUwNzg1fQ.UFUt59SUM2_AW4cRU8Y0BYVQsNTo4n7AFsNrqOpYiICDu37vVt-tw38UKzjmUKtcRsLLjrR3gFW3dNDMx_pL9DVjgVHDdYirtrCekUHOYoa1CMR66nxep5q5cBQ4y4u2kIgSvChCTc9pmLLNoIem-ruCecAJYgI9Ks7pTnW1gkOKs0x3YpiLpzplVHAkkHztaXiJdtpBcY1OXyo6jTQCa3Lk2Q3va1dPkh_d--GU2M5flgd8xNBPYw4vxyt0mP59XZlHMpztZt0soSgObf7G3GXArreF_6tpbFsS3z2t5zkEiHuWJXpzcYr5zWTRPDEHsejeBSG8EgpLDce2380ROQ

发出访问令牌请求

生成已签名的 JWT 后,应用可以使用它来请求访问令牌。 此访问令牌请求是 HTTPS POST 请求,正文采用网址编码格式。网址如下所示:

https://oauth2.googleapis.com/token

在 HTTPS POST 请求中,必须包含以下参数:

名称 说明
grant_type 使用以下字符串(必要时进行网址编码): urn:ietf:params:oauth:grant-type:jwt-bearer
assertion JWT,包括签名。

以下是访问令牌请求中使用的 HTTPS POST 请求的原始转储:

POST /token HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvcHJlZGljdGlvbiIsImF1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsImV4cCI6MTMyODU3MzM4MSwiaWF0IjoxMzI4NTY5NzgxfQ.ixOUGehweEVX_UKXv5BbbwVEdcz6AYS-6uQV6fGorGKrHf3LIJnyREw9evE-gs2bmMaQI5_UbabvI4k-mQE4kBqtmSpTzxYBL1TCd7Kv5nTZoUC1CmwmWCFqT9RE6D7XSgPUh_jF1qskLa2w0rxMSjwruNKbysgRNctZPln7cqQ

以下是使用 curl 的同一请求:

curl -d 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvcHJlZGljdGlvbiIsImF1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsImV4cCI6MTMyODU3MzM4MSwiaWF0IjoxMzI4NTY5NzgxfQ.RZVpzWygMLuL-n3GwjW1_yhQhrqDacyvaXkuf8HcJl8EtXYjGjMaW5oiM5cgAaIorrqgYlp4DPF_GuncFqg9uDZrx7pMmCZ_yHfxhSCXru3gbXrZvAIicNQZMFxrEEn4REVuq7DjkTMyCMGCY1dpMa8aWfTQFt3Eh7smLchaZsU
' https://oauth2.googleapis.com/token

处理响应

如果 JWT 和访问令牌请求格式正确,并且服务账号有权执行相应操作,则授权服务器返回的 JSON 响应会包含访问令牌。以下是一个示例响应:

{
  "access_token": "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M",
  "scope": "https://www.googleapis.com/auth/prediction"
  "token_type": "Bearer",
  "expires_in": 3600
}

expires_in 值指定的时长窗口内,可以重复使用访问令牌。

调用 Google API

Java

使用 GoogleCredentials 对象调用 Google API,请完成以下步骤:

  1. 使用 GoogleCredentials 对象为要调用的 API 创建服务对象。例如:
    SQLAdmin sqladmin =
        new SQLAdmin.Builder(httpTransport, JSON_FACTORY, credentials).build();
  2. 使用服务对象提供的接口向 API 服务发出请求。 例如,如需列出 exciting-example-123 项目中的 Cloud SQL 数据库实例,请执行以下操作:
    SQLAdmin.Instances.List instances =
        sqladmin.instances().list("exciting-example-123").execute();

Python

使用已获授权的 Credentials 对象调用 Google API,方法是完成以下步骤:

  1. 为要调用的 API 构建服务对象。您可以通过调用 build 函数并传入 API 的名称和版本以及已获授权的 Credentials 对象来构建服务对象。例如,如需调用 Cloud SQL Administration API 的 1beta3 版,请执行以下操作:
    import googleapiclient.discovery
    
    sqladmin = googleapiclient.discovery.build('sqladmin', 'v1beta3', credentials=credentials)
  2. 使用服务对象提供的接口向 API 服务发出请求。 例如,如需列出 exciting-example-123 项目中的 Cloud SQL 数据库实例,请执行以下操作:
    response = sqladmin.instances().list(project='exciting-example-123').execute()

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 OAuth 2.0 授权服务器发布的访问令牌会在 expires_in 值所提供的时长过后过期。当访问令牌过期时,应用应生成另一个 JWT 并对其签名,然后请求另一个访问令牌。

JWT 错误代码

error 字段 error_description 字段 含义 如何解决
unauthorized_client Unauthorized client or scope in request. 如果您尝试使用全网域授权,则服务账号在用户网域的管理控制台中未获得授权。

确保服务账号已在管理控制台的 全网域授权页面中针对 sub 声明(字段)中的用户获得授权。

虽然此过程通常需要几分钟的时间,但授权最长可能需要 24 小时才能应用到您 Google 账号中的所有用户。

unauthorized_client Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested. 在管理控制台中,服务账号是使用客户端电子邮件地址(而非客户端 ID [数字])授权的。 在管理控制台的 全网域授权页面中,移除相应客户端,然后使用数字 ID 重新添加该客户端。
access_denied (任意值) 如果您使用的是全网域委派,则管理控制台中未授权一个或多个请求的范围。

确保服务账号已在管理控制台的 全网域委托页面中获得 sub 声明(字段)中用户的授权,并且包含您在 JWT 的 scope 声明中请求的所有范围。

虽然此过程通常需要几分钟的时间,但授权最长可能需要 24 小时才能应用到您 Google 账号中的所有用户。

admin_policy_enforced (任意值) 由于 Google Workspace 管理员的政策,相应 Google 账号无法授权一个或多个请求的范围。

如需详细了解管理员如何限制对所有范围或敏感范围和受限范围的访问权限,直到明确授予您的 OAuth 客户端 ID 访问权限为止,请参阅 Google Workspace 管理员帮助文章控制哪些第三方应用和内部应用可以访问 Google Workspace 数据

invalid_client (任意值)

OAuth 客户端或 JWT 令牌无效或配置不正确。

如需了解详情,请参阅错误说明。

确保 JWT 令牌有效且包含正确的声明。

检查 OAuth 客户端和服务账号是否已正确配置,以及您是否使用了正确的电子邮件地址。

检查 JWT 令牌是否正确,以及是否是为请求中的客户端 ID 颁发的。

deleted_client (任意值)

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

使用仍处于有效状态的客户端 ID。

invalid_grant Not a valid email. 用户不存在。 检查 sub 声明(字段)中的电子邮件地址是否正确。
invalid_grant

Invalid JWT: Token must be a short-lived token (60 minutes) and in a reasonable timeframe. Check your 'iat' and 'exp' values and use a clock with skew to account for clock differences between systems.

这通常意味着本地系统时间不正确。如果 exp 值比 iat 值晚 65 分钟以上,或者 exp 值低于 iat 值,也可能会发生这种情况。

确保生成 JWT 的系统上的时钟正确无误。如有必要,请与 Google NTP 同步时间。

invalid_grant Invalid JWT Signature.

JWT 断言的签名所用的私钥与客户端电子邮件地址所标识的服务账号无关,或者所用的密钥已被删除、停用或已过期。

或者,JWT 断言可能编码不正确 - 它必须采用 Base64 编码,且不包含换行符或填充等号。

对 JWT 声明集进行解码,并验证用于对断言进行签名的密钥是否与服务账号相关联。

尝试使用 Google 提供的 OAuth 库,确保 JWT 正确生成。

invalid_scope Invalid OAuth scope or ID token audience provided. 未请求任何范围(范围列表为空),或者所请求的某个范围不存在(即无效)。

确保 JWT 的 scope 声明(字段)已填充,并将其中包含的作用域与您要使用的 API 的文档化作用域进行比较,以确保没有错误或拼写错误。

请注意,scope 声明中的授权范围列表需要以空格分隔,而不是以英文逗号分隔。

disabled_client The OAuth client was disabled. 用于签署 JWT 断言的密钥已被停用。

前往 Google API Console,然后在 IAM 和管理 > 服务账号 下,启用包含用于签署断言的“密钥 ID”的服务账号。

org_internal This client is restricted to users within its organization. 请求中的 OAuth 客户端 ID 属于一个项目,该项目限制对特定 Google Cloud 组织中的 Google 账号的访问权限。

使用组织的服务账号进行身份验证。确认 OAuth 应用的用户类型配置

附录:不使用 OAuth 的服务账号授权

对于某些 Google API,您可以直接使用已签名的 JWT 作为不记名令牌,而不是 OAuth 2.0 访问令牌,来发出已获授权的 API 调用。如果可以,您无需在发出 API 调用之前向 Google 的授权服务器发出网络请求。

如果您要调用的 API 在 Google API GitHub 代码库中发布了服务定义,则可以使用 JWT(而不是访问令牌)进行授权的 API 调用。为此,请执行以下操作:

  1. 如上所述创建服务账号。请务必保留您在创建账号时获得的 JSON 文件。
  2. 使用任何标准 JWT 库(例如 jwt.io 中的库)创建一个标头和载荷类似于以下示例的 JWT:
    {
      "alg": "RS256",
      "typ": "JWT",
      "kid": "abcdef1234567890"
    }
    .
    {
      "iss": "123456-compute@developer.gserviceaccount.com",
      "sub": "123456-compute@developer.gserviceaccount.com",
      "aud": "https://firestore.googleapis.com/",
      "iat": 1511900000,
      "exp": 1511903600
    }
    • 对于标头中的 kid 字段,请指定服务账号的私钥 ID。您可以在服务账号 JSON 文件的 private_key_id 字段中找到此值。
    • 对于 isssub 字段,请指定服务账号的电子邮件地址。您可以在服务账号 JSON 文件的 client_email 字段中找到此值。
    • 对于 aud 字段,请指定 API 端点。例如:https://SERVICE.googleapis.com/
    • 对于 iat 字段,请指定当前的 Unix 时间;对于 exp 字段,请指定 3600 秒后的时间,届时 JWT 将过期。

使用服务账号 JSON 文件中的私钥通过 RSA-256 对 JWT 进行签名。

例如:

Java

使用 google-auth-library-javajava-jwt

import com.google.auth.oauth2.ServiceAccountCredentials;
...
GoogleCredentials credentials =
        GoogleCredentials.fromStream(new FileInputStream("MyProject-1234.json"));
PrivateKey privateKey = ((ServiceAccountCredentials) credentials).getPrivateKey();
String privateKeyId = ((ServiceAccountCredentials) credentials).getPrivateKeyId();

long now = System.currentTimeMillis();

try {
    Algorithm algorithm = Algorithm.RSA256(null, privateKey);
    String signedJwt = JWT.create()
        .withKeyId(privateKeyId)
        .withIssuer("123456-compute@developer.gserviceaccount.com")
        .withSubject("123456-compute@developer.gserviceaccount.com")
        .withAudience("https://firestore.googleapis.com/")
        .withIssuedAt(new Date(now))
        .withExpiresAt(new Date(now + 3600 * 1000L))
        .sign(algorithm);
} catch ...

Python

使用 PyJWT

iat = time.time()
exp = iat + 3600
payload = {'iss': '123456-compute@developer.gserviceaccount.com',
           'sub': '123456-compute@developer.gserviceaccount.com',
           'aud': 'https://firestore.googleapis.com/',
           'iat': iat,
           'exp': exp}
additional_headers = {'kid': PRIVATE_KEY_ID_FROM_JSON}
signed_jwt = jwt.encode(payload, PRIVATE_KEY_FROM_JSON, headers=additional_headers,
                       algorithm='RS256')
  1. 调用 API,使用已签名的 JWT 作为不记名令牌:
    GET /v1/projects/abc/databases/123/indexes HTTP/1.1
    Authorization: Bearer SIGNED_JWT
    Host: firestore.googleapis.com

实现跨账号保护

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

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