使用令牌模型

google.accounts.oauth2 JavaScript 库可帮助您提示用户同意并获取访问令牌,以处理用户数据。它基于 OAuth 2.0 隐式授权流程,旨在允许您直接使用 REST 和 CORS 调用 Google API,或者使用我们的适用于 JavaScript 的 Google API 客户端库(也称为 gapi.client),以便简单灵活地访问我们更复杂的 API。

在通过浏览器访问受保护的用户数据之前,您网站上的用户会触发 Google 网页版帐号选择器、登录和意见征求流程,最后,Google 的 OAuth 服务器会发出并返回访问令牌给您的 Web 应用。

在基于令牌的授权模型中,无需在后端服务器上存储按用户的刷新令牌。

建议您按照此处介绍的方法操作,而不是旧版适用于客户端 Web 应用的 OAuth 2.0 指南中介绍的技术。

初始设置

按照获取您的 Google API 客户端 ID 指南中的步骤查找或创建客户端 ID。接下来,将客户端库添加到您网站上将调用 Google API 的网页。最后,初始化令牌客户端。这通常在客户端库的 onload 处理程序中完成。

初始化令牌客户端

调用 initTokenClient() 以使用 Web 应用的客户端 ID 初始化新的令牌客户端,或者,您可以选择包含用户需要访问的一个或多个范围的列表:

const client = google.accounts.oauth2.initTokenClient({
  client_id: 'YOUR_GOOGLE_CLIENT_ID',
  scope: 'https://www.googleapis.com/auth/calendar.readonly',
  callback: (response) => {
    ...
  },
});

触发 OAuth 2.0 令牌流程

使用 requestAccessToken() 方法触发令牌用户体验流程并获取访问令牌。Google 会提示用户:

  • 选择孩子的帐号,
  • 登录 Google 帐号(如果尚未登录);
  • 同意您的 Web 应用访问所请求的每个范围。

用户手势触发令牌流程:<button onclick="client.requestAccessToken();">Authorize me</button>

然后,Google 会向回调处理程序返回一个 TokenResponse,其中包含访问令牌以及用户已授予访问权限的范围列表或错误。

用户可能会关闭帐号选择器或登录窗口,在这种情况下,系统不会调用您的回调函数。

只有在仔细审核 Google 的 OAuth 2.0 政策之后,您才能为自己的应用实现设计和用户体验。这些政策涵盖了使用多个范围、何时以及如何处理用户意见征求等。

增量授权是一种政策和应用设计方法,用于根据范围仅在需要时(而不是一次性)请求访问资源。用户可以批准或拒绝共享您的应用所请求的各项资源,这就是所谓的精细权限

在此过程中,Google 会提示用户同意(分别列出每个请求的范围),用户选择要与您的应用共享的资源,最后,Google 会调用您的回调函数以返回访问令牌和用户批准的范围。然后,您的应用可以使用精细权限安全地处理可能出现的各种不同结果。

递增授权

对于 Web 应用,以下两个主要场景演示了使用以下内容的增量授权:

  • 单页 Ajax 应用,通常使用可动态访问资源的 XMLHttpRequest
  • 多个网页,资源分离,并且基于每个页面进行管理。

我们展示这两种场景是为了说明设计注意事项和方法,但并未就如何将用户意见征求机制构建到应用中提供全面建议。实际应用可能会使用这两种技术的变体或组合。

开普敦阿贾克斯

通过多次调用 requestAccessToken() 并使用 OverridableTokenClientConfig 对象的 scope 参数,在需要且仅在必要时使用各个范围请求单个范围,为您的应用添加对增量授权的支持。在此示例中,只有在用户手势展开收起的内容部分后,系统才会请求这些资源,并且这些资源仅在用户手势展开后可见。

Ajax 应用
在网页加载时初始化令牌客户端:
        const client = google.accounts.oauth2.initTokenClient({
          client_id: 'YOUR_GOOGLE_CLIENT_ID',
          callback: "onTokenResponse",
        });
      
请求用户同意并通过用户手势获取访问令牌,点击“+”即可打开:

要阅读的文档

显示近期文档

          client.requestAccessToken(
            overrideConfig = ({
               scope = 'https://www.googleapis.com/auth/documents.readonly'
             })
           );
        

活动预告

显示日历信息

          client.requestAccessToken(
            overrideConfig = ({
               scope = 'https://www.googleapis.com/auth/calendar.readonly'
             })
           );
        

显示照片

          client.requestAccessToken(
            overrideConfig = ({
               scope = 'https://www.googleapis.com/auth/photoslibrary.readonly'
             })
           );
        

每次调用 requestAccessToken 都会触发表示同意的时刻,您的应用只能访问用户选择展开的部分所需的资源,因此通过用户选择的方式共享资源会受到限制。

多个网页

针对增量授权进行设计时,系统会使用多个页面来仅请求加载页面所需的范围,从而降低复杂性并减少进行多次调用以征得用户同意和检索访问令牌的需求。

多页应用
网页 编码
第 1 页建议阅读的文档
  const client = google.accounts.oauth2.initTokenClient({
    client_id: 'YOUR_GOOGLE_CLIENT_ID',
    callback: "onTokenResponse",
    scope: 'https://www.googleapis.com/auth/documents.readonly',
  });
  client.requestAccessToken();
          
第 2 页:即将举行的活动
  const client = google.accounts.oauth2.initTokenClient({
    client_id: 'YOUR_GOOGLE_CLIENT_ID',
    callback: "onTokenResponse",
    scope: 'https://www.googleapis.com/auth/calendar.readonly',
  });
  client.requestAccessToken();
          
第 3 页:照片轮播界面
  const client = google.accounts.oauth2.initTokenClient({
    client_id: 'YOUR_GOOGLE_CLIENT_ID',
    callback: "onTokenResponse",
    scope: 'https://www.googleapis.com/auth/photoslibrary.readonly',
  });
  client.requestAccessToken();
          

每个页面都会请求必要的范围,并在加载时调用 initTokenClient()requestAccessToken() 来获取访问令牌。在这种情况下,各个网页用于按范围明确区分用户功能和资源。在实际情况下,各个页面可能会请求多个相关范围。

细化权限

在所有场景中,细化权限的处理方式相同;在 requestAccessToken() 调用您的回调函数并且返回访问令牌后,检查用户是否已批准使用 hasGrantedAllScopes()hasGrantedAnyScope() 请求的范围。例如:

const client = google.accounts.oauth2.initTokenClient({
  client_id: 'YOUR_GOOGLE_CLIENT_ID',
  scope: 'https://www.googleapis.com/auth/calendar.readonly \
          https://www.googleapis.com/auth/documents.readonly \
          https://www.googleapis.com/auth/photoslibrary.readonly',
  callback: (tokenResponse) => {
    if (tokenResponse && tokenResponse.access_token) {
      if (google.accounts.oauth2.hasGrantedAnyScope(tokenResponse,
          'https://www.googleapis.com/auth/photoslibrary.readonly')) {
        // Look at pictures
        ...
      }
      if (google.accounts.oauth2.hasGrantedAllScopes(tokenResponse,
          'https://www.googleapis.com/auth/calendar.readonly',
          'https://www.googleapis.com/auth/documents.readonly')) {
        // Meeting planning and review documents
        ...
      }
    }
  },
});

之前会话或请求中的任何以前接受的授权也将包含在响应中。系统会为每个用户和客户端 ID 保留一份用户意见征求记录,并且会在多次调用 initTokenClient()requestAccessToken() 时保留这一记录。默认情况下,只有在用户首次访问您的网站并请求新范围时才需要征得用户同意,但在每次网页加载时,可以使用 Token Client 配置对象中的 prompt=consent 来征求用户同意。

使用令牌

在令牌模型中,操作系统或浏览器不会存储访问令牌,而是在网页加载时首次获取新令牌,或随后通过用户手势(例如按下按钮)触发对 requestAccessToken() 的调用。

将 REST 和 CORS 与 Google API 搭配使用

访问令牌可用于使用 REST 和 CORS 向 Google API 发出经过身份验证的请求。这样,用户就可以登录和征得用户同意,然后 Google 会颁发访问令牌并您的网站处理用户数据。

在以下示例中,使用 tokenRequest() 返回的访问令牌查看已登录用户即将进行的日历活动:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.googleapis.com/calendar/v3/calendars/primary/events');
xhr.setRequestHeader('Authorization', 'Bearer ' + tokenResponse.access_token);
xhr.send();

如需了解详情,请参阅如何使用 CORS 访问 Google API

下一部分将介绍如何轻松与更复杂的 API 集成。

使用 Google API JavaScript 库

令牌客户端可与适用于 JavaScript 的 Google API 客户端库搭配使用。请参阅以下代码段。

const client = google.accounts.oauth2.initTokenClient({
  client_id: 'YOUR_GOOGLE_CLIENT_ID',
  scope: 'https://www.googleapis.com/auth/calendar.readonly',
  callback: (tokenResponse) => {
    if (tokenResponse && tokenResponse.access_token) {
      gapi.client.setApiKey('YOUR_API_KEY');
      gapi.client.load('calendar', 'v3', listUpcomingEvents);
    }
  },
});

function listUpcomingEvents() {
  gapi.client.calendar.events.list(...);
}

令牌过期

根据设计,访问令牌的生命周期较短。如果访问令牌在用户会话结束之前到期,请通过从用户驱动的事件(例如按下按钮)调用 requestAccessToken() 来获取新令牌。

调用 google.accounts.oauth2.revoke 方法,对于已向您的应用授予的所有范围,撤消用户同意情况以及对资源的访问权限。撤消此权限需要使用有效的访问令牌:

google.accounts.oauth2.revoke('414a76cb127a7ece7ee4bf287602ca2b56f8fcbf7fcecc2cd4e0509268120bd7', done => {
    console.log(done);
    console.log(done.successful);
    console.log(done.error);
    console.log(done.error_description);
  });