迁移到 Google Identity 服务

概览

为了获取用于调用 Google API 的用户专用访问令牌,Google 提供了多个 JavaScript 库:

本指南提供了从这些库迁移到 Google Identity 服务库的说明。

按照本指南操作,您将:

  • 将已弃用的平台库替换为 Identity 服务库,并且
  • 如果使用 API 客户端库,请移除已弃用的 gapi.auth2 模块及其方法和对象,并将其替换为 Identity Services 等效项。

如需了解 Identity Services JavaScript 库的更改,请参阅概览用户授权的运作方式,以查看关键术语和概念。

如果您要为用户注册和登录寻找身份验证,请参阅从 Google 登录迁移

确定您的授权流程

用户授权流程有两种:隐式授权和授权代码授权。

检查您的 Web 应用,以确定所用的授权流程类型。

表明您的 Web 应用正在使用隐式流程的迹象:

表明您的 Web 应用正在使用授权代码流程的迹象:

  • 您的实现基于以下内容:

  • 您的应用既在用户的浏览器中执行,也在您的后端平台上执行。

  • 您的后端平台托管着授权代码端点。

  • 您的后端平台代表用户调用 Google API,而无需用户在场,这称为离线模式。

  • 刷新令牌由您的后端平台管理和存储。

在某些情况下,您的代码库可能同时支持这两种流程。

选择授权流程

在开始迁移之前,您需要确定继续使用现有流程还是采用其他流程最能满足您的需求。

请参阅选择授权流程,了解这两种流程之间的主要区别和权衡取舍。

在大多数情况下,建议使用授权代码流程,因为该流程可提供最高级别的用户安全性。实现此流程还可让您的平台添加新的离线功能,例如提取更新,以便在用户的日历、照片和订阅发生重大变化时通知用户。

使用选择器选择授权流程。

隐式流

在用户在场时获取供浏览器内使用的访问令牌。

隐式流程示例展示了迁移到身份服务前后的 Web 应用。

授权代码流程

Google 针对每个用户发放的授权代码会发送到您的后端平台,然后在此处换取访问令牌和刷新令牌。

授权代码流程示例展示了迁移到身份服务前后的 Web 应用。

在本指南中,请按照以粗体列出的说明添加移除更新替换现有功能。

对浏览器内 Web 应用的更改

本部分将介绍在迁移到 Google Identity Services JavaScript 库时,您将对浏览器内 Web 应用做出的更改。

确定受影响的代码并进行测试

调试 Cookie 有助于找到受影响的代码并测试弃用后的行为。

在大型或复杂的应用中,可能很难找到受 gapi.auth2 模块弃用影响的所有代码。如需将即将弃用的功能的现有使用情况记录到控制台,请将 G_AUTH2_MIGRATION cookie 的值设置为 informational。(可选)添加一个英文冒号,后跟一个键值,以便同时记录到会话存储空间。登录后,接收凭据审核结果或将收集的日志发送到后端以供日后分析。例如,informational:showauth2use 将来源和网址保存到名为 showauth2use 的会话存储键中。

如需验证应用在 gapi.auth2 模块不再加载时的行为,请将 G_AUTH2_MIGRATION Cookie 的值设置为 enforced。这样一来,您就可以在强制执行日期之前测试弃用后的行为。

可能的 G_AUTH2_MIGRATION Cookie 值:

  • enforced 请勿加载 gapi.auth2 模块。
  • informational 将已弃用功能的使用情况记录到 JS 控制台。当设置了可选的键名时,也记录到会话存储空间:informational:key-name

为尽可能减少对用户的影响,建议您先在开发和测试期间本地设置此 Cookie,然后再在生产环境中使用。

库和模块

gapi.auth2 模块用于管理登录的用户身份验证和授权的隐式流程,请将此已弃用的模块及其对象和方法替换为 Google Identity 服务库。

通过在文档中添加 Identity Services 库,将其添加到您的 Web 应用:

<script src="https://accounts.google.com/gsi/client" async defer></script>

移除使用 gapi.load('auth2', function) 加载 auth2 模块的所有实例。

Google Identity 服务库取代了 gapi.auth2 模块的使用。 您可以继续安全地使用 Google API JavaScript 客户端库中的 gapi.client 模块,并利用其自动从发现文档创建可调用的 JS 方法、批量处理多个 API 调用以及 CORS 管理功能。

Cookie

用户授权不需要使用 Cookie。

如需详细了解用户身份验证如何使用 Cookie,请参阅从 Google 登录迁移;如需了解其他 Google 产品和服务如何使用 Cookie,请参阅 Google 如何使用 Cookie

凭证

Google Identity Services 将用户身份验证和授权分为两个不同的操作,并且用户凭据是分开的:用于标识用户的 ID 令牌与用于授权的访问令牌是分开返回的。

如需查看这些更改,请参阅示例凭据

隐式流

通过从授权流程中移除用户个人资料处理,分离用户身份验证和授权。

移除以下 Google 登录 JavaScript 客户端参考

方法

  • GoogleUser.getBasicProfile()
  • GoogleUser.getId()

授权代码流程

身份服务将浏览器内凭据分为 ID 令牌和访问令牌。此项更改不适用于通过以下方式获得的凭据:直接从后端平台调用 Google OAuth 2.0 端点,或通过在平台上的安全服务器上运行的库(例如 Google APIs Node.js Client)获得凭据。

会话状态

以前,Google 登录服务可帮助您使用以下方式管理用户登录状态:

您负责管理 Web 应用的登录状态和用户会话。

移除以下 Google 登录 JavaScript 客户端参考

对象:

  • gapi.auth2.SignInOptions

方法:

  • GoogleAuth.attachClickHandler()
  • GoogleAuth.isSignedIn()
  • GoogleAuth.isSignedIn.get()
  • GoogleAuth.isSignedIn.listen()
  • GoogleAuth.signIn()
  • GoogleAuth.signOut()
  • GoogleAuth.currentUser.get()
  • GoogleAuth.currentUser.listen()
  • GoogleUser.isSignedIn()

客户端配置

更新您的 Web 应用,以初始化隐式或授权码流程的令牌客户端。

移除以下 Google 登录 JavaScript 客户端参考

对象:

  • gapi.auth2.ClientConfig
  • gapi.auth2.OfflineAccessOptions

方法:

  • gapi.auth2.getAuthInstance()
  • GoogleUser.grant()

隐式流

按照初始化令牌客户端中的示例,添加 TokenClientConfig 对象和 initTokenClient() 调用来配置您的 Web 应用。

Google 登录 JavaScript 客户端参考替换为 Google Identity 服务

对象:

  • gapi.auth2.AuthorizeConfigTokenClientConfig 合作

方法:

  • gapi.auth2.init()google.accounts.oauth2.initTokenClient() 合作

参数:

  • gapi.auth2.AuthorizeConfig.login_hint 以及 TokenClientConfig.login_hint
  • 使用 TokenClientConfig.hd 调用 gapi.auth2.GoogleUser.getHostedDomain()

授权代码流程

按照初始化代码客户端中的示例,添加 CodeClientConfig 对象和 initCodeClient() 调用来配置 Web 应用。

从隐式授权流程切换到授权代码流程时:

移除 Google 登录 JavaScript 客户端参考

对象:

  • gapi.auth2.AuthorizeConfig

方法:

  • gapi.auth2.init()

参数:

  • gapi.auth2.AuthorizeConfig.login_hint
  • gapi.auth2.GoogleUser.getHostedDomain()

令牌请求

用户手势(例如点击按钮)会生成一个请求,该请求会通过隐式流程直接向用户的浏览器返回访问令牌,或者在将每个用户的授权代码换成访问令牌和刷新令牌后,向您的后端平台返回访问令牌。

隐式流

当用户已登录 Google 且与 Google 之间存在有效会话时,可以在浏览器中获取和使用访问令牌。对于隐式模式,即使之前有请求,也需要用户手势来请求访问令牌。

替换 Google 登录 JavaScript 客户端参考:使用 Google Identity 服务

方法:

  • gapi.auth2.authorize()TokenClient.requestAccessToken() 合作
  • GoogleUser.reloadAuthResponse() 以及 TokenClient.requestAccessToken()

添加链接或按钮以调用 requestAccessToken() 来启动弹出式用户体验流程,以请求访问令牌,或在现有令牌过期时获取新令牌。

更新代码库,以便:

  • 使用 requestAccessToken() 触发 OAuth 2.0 令牌流程
  • 使用 requestAccessTokenOverridableTokenClientConfig 将针对多个范围的单个请求拆分为多个较小的请求,以支持增量授权。
  • 在现有令牌过期或被撤消时,请求新令牌。

使用多个范围可能需要对代码库进行结构性更改,以便仅在需要时请求范围访问权限,而不是一次性请求所有范围访问权限,这称为增量授权。每个请求应包含尽可能少的范围,最好只有一个范围。如需详细了解如何更新应用以实现增量授权,请参阅如何处理用户意见征求

当访问令牌过期时,gapi.auth2 模块会自动为您的 Web 应用获取新的有效访问令牌。为了提高用户安全性,Google Identity Services 库不支持这种自动令牌刷新流程。您必须更新 Web 应用,以检测过期的访问令牌并请求新令牌。如需了解详情,请参阅“令牌处理”部分。

授权代码流程

添加一个链接或按钮,以调用 requestCode() 来向 Google 请求授权码。如需查看示例,请参阅触发 OAuth 2.0 代码流程

如需详细了解如何应对过期或被撤消的访问令牌,请参阅“令牌处理”部分。

令牌处理

添加了错误处理功能,以检测在使用过期或已撤消的访问令牌时失败的 Google API 调用,并请求新的有效访问令牌。

当使用过期或被撤消的访问令牌时,Google API 会返回 HTTP 状态代码 401 Unauthorizedinvalid_token 错误消息。如需查看示例,请参阅无效令牌响应

已过期的令牌

访问令牌的有效期很短,通常只有几分钟。

令牌撤消

Google 账号所有者可以随时撤消之前授予的同意声明。这样做会使现有的访问令牌和刷新令牌失效。您可以使用 revoke() 从平台触发撤消,也可以通过 Google 账号触发撤消。

替换 Google 登录 JavaScript 客户端参考:使用 Google Identity 服务

方法:

  • getAuthInstance().disconnect()google.accounts.oauth2.revoke() 合作
  • GoogleUser.disconnect()google.accounts.oauth2.revoke() 合作

当用户在您的平台上删除其账号,或希望撤消向您的应用分享数据的同意声明时,请调用 revoke

当您的 Web 应用或后端平台请求访问令牌时,Google 会向用户显示意见征求对话框。请参阅 Google 向用户显示的意见征求对话框示例。

在向您的应用发放访问令牌之前,系统需要存在有效的 Google 会话,以便提示用户授予同意权限并记录结果。如果尚未建立现有会话,则可能需要用户登录 Google 账号。

用户登录

用户可能在单独的浏览器标签页中登录了 Google 账号,或者通过浏览器或操作系统以原生方式登录了 Google 账号。我们建议您向网站添加使用 Google 账号登录功能,以便在用户首次打开您的应用时,在 Google 账号与浏览器之间建立有效会话。这样做可带来以下好处:

  • 尽量减少用户必须登录的次数,请求访问令牌会在尚无有效会话时启动 Google 账号登录流程。
  • 直接使用 JWT ID 令牌凭据 email 字段作为 CodeClientConfigTokenClientConfig 对象中 login_hint 参数的值。如果您的平台不维护用户账号管理系统,此功能会特别有用。
  • 查找 Google 账号并将其与平台上的现有本地用户账号相关联,有助于最大限度地减少平台上的重复账号。
  • 创建新的本地账号时,注册对话框和流程可以与用户身份验证对话框和流程清晰分离,从而减少所需步骤的数量并提高完成率。

登录后,在颁发访问令牌之前,用户必须针对所请求的范围向您的应用授予同意。

在用户同意后,系统会返回访问令牌以及用户批准或拒绝的范围列表。

精细的权限可让用户批准或拒绝各个范围。当请求访问多个范围时,每个范围的授予或拒绝与其他范围无关。根据用户选择,您的应用会选择性地启用依赖于特定范围的功能。

隐式流

Google 登录 JavaScript 客户端参考替换为 Google Identity 服务

对象:

  • gapi.auth2.AuthorizeResponseTokenClient.TokenResponse 合作
  • gapi.auth2.AuthResponseTokenClient.TokenResponse 合作

方法:

  • GoogleUser.hasGrantedScopes() 以及 google.accounts.oauth2.hasGrantedAllScopes()
  • GoogleUser.getGrantedScopes() 以及 google.accounts.oauth2.hasGrantedAllScopes()

移除 Google 登录 JavaScript 客户端参考

方法:

  • GoogleUser.getAuthResponse()

按照此精细权限示例,使用 hasGrantedAllScopes()hasGrantedAnyScope() 更新您的 Web 应用。

授权代码流程

按照授权代码处理中的说明,更新添加授权代码端点到您的后端平台。

更新您的平台,按照使用代码模型指南中所述的步骤验证请求并获取访问令牌和刷新令牌。

更新您的平台,以根据用户批准的各个范围选择性地启用或停用功能,方法是按照增量授权的说明操作,并检查用户授予的访问权限范围

隐式流示例

旧方法

GAPI 客户端库

JavaScript 版 Google API 客户端库在浏览器中运行的示例,使用弹出式对话框征求用户同意。

gapi.auth2 模块由 gapi.client.init() 自动加载和使用,因此处于隐藏状态。

<!DOCTYPE html>
  <html>
    <head>
      <script src="https://apis.google.com/js/api.js"></script>
      <script>
        function start() {
          gapi.client.init({
            'apiKey': 'YOUR_API_KEY',
            'clientId': 'YOUR_CLIENT_ID',
            'scope': 'https://www.googleapis.com/auth/cloud-translation',
            'discoveryDocs': ['https://www.googleapis.com/discovery/v1/apis/translate/v2/rest'],
          }).then(function() {
            // Execute an API request which is returned as a Promise.
            // The method name language.translations.list comes from the API discovery.
            return gapi.client.language.translations.list({
              q: 'hello world',
              source: 'en',
              target: 'de',
            });
          }).then(function(response) {
            console.log(response.result.data.translations[0].translatedText);
          }, function(reason) {
            console.log('Error: ' + reason.result.error.message);
          });
        };

        // Load the JavaScript client library and invoke start afterwards.
        gapi.load('client', start);
      </script>
    </head>
    <body>
      <div id="results"></div>
    </body>
  </html>

JS 客户端库

适用于客户端 Web 应用的 OAuth 2.0,在浏览器中运行,使用弹出式对话框征求用户同意。

gapi.auth2 模块是手动加载的。

<!DOCTYPE html>
<html><head></head><body>
<script>
  var GoogleAuth;
  var SCOPE = 'https://www.googleapis.com/auth/drive.metadata.readonly';
  function handleClientLoad() {
    // Load the API's client and auth2 modules.
    // Call the initClient function after the modules load.
    gapi.load('client:auth2', initClient);
  }

  function initClient() {
    // In practice, your app can retrieve one or more discovery documents.
    var discoveryUrl = 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest';

    // Initialize the gapi.client object, which app uses to make API requests.
    // Get API key and client ID from Google Cloud console.
    // 'scope' field specifies space-delimited list of access scopes.
    gapi.client.init({
        'apiKey': 'YOUR_API_KEY',
        'clientId': 'YOUR_CLIENT_ID',
        'discoveryDocs': [discoveryUrl],
        'scope': SCOPE
    }).then(function () {
      GoogleAuth = gapi.auth2.getAuthInstance();

      // Listen for sign-in state changes.
      GoogleAuth.isSignedIn.listen(updateSigninStatus);

      // Handle initial sign-in state. (Determine if user is already signed in.)
      var user = GoogleAuth.currentUser.get();
      setSigninStatus();

      // Call handleAuthClick function when user clicks on
      //      "Sign In/Authorize" button.
      $('#sign-in-or-out-button').click(function() {
        handleAuthClick();
      });
      $('#revoke-access-button').click(function() {
        revokeAccess();
      });
    });
  }

  function handleAuthClick() {
    if (GoogleAuth.isSignedIn.get()) {
      // User is authorized and has clicked "Sign out" button.
      GoogleAuth.signOut();
    } else {
      // User is not signed in. Start Google auth flow.
      GoogleAuth.signIn();
    }
  }

  function revokeAccess() {
    GoogleAuth.disconnect();
  }

  function setSigninStatus() {
    var user = GoogleAuth.currentUser.get();
    var isAuthorized = user.hasGrantedScopes(SCOPE);
    if (isAuthorized) {
      $('#sign-in-or-out-button').html('Sign out');
      $('#revoke-access-button').css('display', 'inline-block');
      $('#auth-status').html('You are currently signed in and have granted ' +
          'access to this app.');
    } else {
      $('#sign-in-or-out-button').html('Sign In/Authorize');
      $('#revoke-access-button').css('display', 'none');
      $('#auth-status').html('You have not authorized this app or you are ' +
          'signed out.');
    }
  }

  function updateSigninStatus() {
    setSigninStatus();
  }
</script>

<button id="sign-in-or-out-button"
        style="margin-left: 25px">Sign In/Authorize</button>
<button id="revoke-access-button"
        style="display: none; margin-left: 25px">Revoke access</button>

<div id="auth-status" style="display: inline; padding-left: 25px"></div><hr>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script async defer src="https://apis.google.com/js/api.js"
        onload="this.onload=function(){};handleClientLoad()"
        onreadystatechange="if (this.readyState === 'complete') this.onload()">
</script>
</body></html>

OAuth 2.0 端点

适用于客户端 Web 应用的 OAuth 2.0,在浏览器中运行,通过重定向到 Google 来征得用户同意。

此示例展示了从用户浏览器直接调用 Google 的 OAuth 2.0 端点,未使用 gapi.auth2 模块或 JavaScript 库。

<!DOCTYPE html>
<html><head></head><body>
<script>
  var YOUR_CLIENT_ID = 'REPLACE_THIS_VALUE';
  var YOUR_REDIRECT_URI = 'REPLACE_THIS_VALUE';
  var fragmentString = location.hash.substring(1);

  // Parse query string to see if page request is coming from OAuth 2.0 server.
  var params = {};
  var regex = /([^&=]+)=([^&]*)/g, m;
  while (m = regex.exec(fragmentString)) {
    params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
  }
  if (Object.keys(params).length > 0) {
    localStorage.setItem('oauth2-test-params', JSON.stringify(params) );
    if (params['state'] && params['state'] == 'try_sample_request') {
      trySampleRequest();
    }
  }

  // If there's an access token, try an API request.
  // Otherwise, start OAuth 2.0 flow.
  function trySampleRequest() {
    var params = JSON.parse(localStorage.getItem('oauth2-test-params'));
    if (params && params['access_token']) {
      var xhr = new XMLHttpRequest();
      xhr.open('GET',
          'https://www.googleapis.com/drive/v3/about?fields=user&' +
          'access_token=' + params['access_token']);
      xhr.onreadystatechange = function (e) {
        if (xhr.readyState === 4 && xhr.status === 200) {
          console.log(xhr.response);
        } else if (xhr.readyState === 4 && xhr.status === 401) {
          // Token invalid, so prompt for user permission.
          oauth2SignIn();
        }
      };
      xhr.send(null);
    } else {
      oauth2SignIn();
    }
  }

  /*

    *   Create form to request access token from Google's OAuth 2.0 server.
 */
function oauth2SignIn() {
  // Google's OAuth 2.0 endpoint for requesting an access token
  var oauth2Endpoint = 'https://accounts.google.com/o/oauth2/v2/auth';

    // Create element to open OAuth 2.0 endpoint in new window.
    var form = document.createElement('form');
    form.setAttribute('method', 'GET'); // Send as a GET request.
    form.setAttribute('action', oauth2Endpoint);

    // Parameters to pass to OAuth 2.0 endpoint.
    var params = {'client_id': YOUR_CLIENT_ID,
                  'redirect_uri': YOUR_REDIRECT_URI,
                  'scope': 'https://www.googleapis.com/auth/drive.metadata.readonly',
                  'state': 'try_sample_request',
                  'include_granted_scopes': 'true',
                  'response_type': 'token'};

    // Add form parameters as hidden input values.
    for (var p in params) {
      var input = document.createElement('input');
      input.setAttribute('type', 'hidden');
      input.setAttribute('name', p);
      input.setAttribute('value', params[p]);
      form.appendChild(input);
    }

    // Add form to page and submit it to open the OAuth 2.0 endpoint.
    document.body.appendChild(form);
    form.submit();
  }
</script>

<button onclick="trySampleRequest();">Try sample request</button>
</body></html>

新方式

仅限 GIS

此示例仅展示了使用令牌模型和弹出式对话框征求用户同意的 Google Identity 服务 JavaScript 库。它旨在说明配置客户端、请求和获取访问令牌以及调用 Google API 所需的最少步骤。

<!DOCTYPE html>
<html>
  <head>
    <script src="https://accounts.google.com/gsi/client" onload="initClient()" async defer></script>
  </head>
  <body>
    <script>
      var client;
      var access_token;

      function initClient() {
        client = google.accounts.oauth2.initTokenClient({
          client_id: 'YOUR_CLIENT_ID',
          scope: 'https://www.googleapis.com/auth/calendar.readonly \
                  https://www.googleapis.com/auth/contacts.readonly',
          callback: (tokenResponse) => {
            access_token = tokenResponse.access_token;
          },
        });
      }
      function getToken() {
        client.requestAccessToken();
      }
      function revokeToken() {
        google.accounts.oauth2.revoke(access_token, () => {console.log('access token revoked')});
      }
      function loadCalendar() {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', 'https://www.googleapis.com/calendar/v3/calendars/primary/events');
        xhr.setRequestHeader('Authorization', 'Bearer ' + access_token);
        xhr.send();
      }
    </script>
    <h1>Google Identity Services Authorization Token model</h1>
    <button onclick="getToken();">Get access token</button><br><br>
    <button onclick="loadCalendar();">Load Calendar</button><br><br>
    <button onclick="revokeToken();">Revoke token</button>
  </body>
</html>

GAPI async/await

此示例展示了如何使用令牌模型添加 Google Identity 服务库、移除 gapi.auth2 模块,以及使用 JavaScript 版 Google API 客户端库调用 API。

使用 Promise、async 和 await 来强制执行库加载顺序,并捕获和重试授权错误。只有在获得有效的访问令牌后,才能进行 API 调用。

如果页面首次加载时访问令牌缺失,或者访问令牌在之后过期,用户应按“显示日历”按钮。

<!DOCTYPE html>
<html>
<head>
    <title>GAPI and GIS Example</title>
    <script async defer src="https://apis.google.com/js/api.js" onload="gapiLoad()"></script>
    <script async defer src="https://accounts.google.com/gsi/client" onload="gisLoad()"></script>
</head>
<body>
    <h1>GAPI Client with GIS Authorization</h1>
    <button id="authorizeBtn" style="visibility:hidden;">Authorize and Load Events</button>
    <button id="revokeBtn" style="visibility:hidden;">Revoke Access</button>
    <div id="content"></div>

    <script>
        const YOUR_CLIENT_ID = "YOUR_CLIENT_ID";
        const YOUR_API_KEY = 'YOUR_API_KEY';
        const CALENDAR_SCOPE = 'https://www.googleapis.com/auth/calendar.readonly';

        let tokenClient;
        let libsLoaded = 0;

        function gapiLoad() {
            gapi.load('client', initGapiClient);
        }

        async function initGapiClient() {
            try {
                await gapi.client.init({ apiKey: YOUR_API_KEY });
                await gapi.client.load('https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest');
                console.log('GAPI client initialized.');
                checkAllLoaded();
            } catch (err) {
                handleError('GAPI initialization failed:', err);
            }
        }

        function gisLoad() {
            try {
                tokenClient = google.accounts.oauth2.initTokenClient({
                    client_id: YOUR_CLIENT_ID,
                    scope: CALENDAR_SCOPE,
                    callback: '', // Will be set dynamically
                    error_callback: handleGisError,
                });
                console.log('GIS TokenClient initialized.');
                checkAllLoaded();
            } catch (err) {
                handleError('GIS initialization failed:', err);
            }
        }

        function checkAllLoaded() {
            libsLoaded++;
            if (libsLoaded === 2) {
                document.getElementById('authorizeBtn').style.visibility = 'visible';
                document.getElementById('revokeBtn').style.visibility = 'visible';
                document.getElementById('authorizeBtn').onclick = makeApiCall;
                document.getElementById('revokeBtn').onclick = revokeAccess;
                console.log('Ready to authorize.');
            }
        }

        function handleGisError(err) {
            console.error('GIS Error:', err);
            let message = 'An error occurred during authorization.';
            if (err && err.type === 'popup_failed_to_open') {
                message = 'Failed to open popup. Please disable popup blockers.';
            } else if (err && err.type === 'popup_closed') {
                message = 'Authorization popup was closed.';
            }
            document.getElementById('content').textContent = message;
        }

        function handleError(message, error) {
            console.error(message, error);
            document.getElementById('content').textContent = `${message} ${error.message || JSON.stringify(error)}`;
        }

        async function makeApiCall() {
            document.getElementById('content').textContent = 'Processing...';
            try {
                let token = gapi.client.getToken();
                if (!token || !token.access_token) {
                    console.log('No token, fetching one...');
                    await getToken();
                }

                console.log('Calling Calendar API...');
                const response = await gapi.client.calendar.events.list({ 'calendarId': 'primary' });
                displayEvents(response.result);
            } catch (err) {
                console.error('API call failed:', err);
                const errorInfo = err.result && err.result.error;
                if (errorInfo && (errorInfo.code === 401 || (errorInfo.code === 403 && errorInfo.status === "PERMISSION_DENIED"))) {
                    console.log('Auth error on API call, refreshing token...');
                    try {
                        await getToken({ prompt: 'consent' }); // Force refresh
                        const retryResponse = await gapi.client.calendar.events.list({ 'calendarId': 'primary' });
                        displayEvents(retryResponse.result);
                    } catch (refreshErr) {
                        handleError('Failed to refresh token or retry API call:', refreshErr);
                    }
                } else {
                    handleError('Error loading events:', err.result ? err.result.error : err);
                }
            }
        }

        async function getToken(options = { prompt: '' }) {
            return new Promise((resolve, reject) => {
                if (!tokenClient) return reject(new Error("GIS TokenClient not initialized."));
                tokenClient.callback = (tokenResponse) => {
                    if (tokenResponse.error) {
                        reject(new Error(`Token Error: ${tokenResponse.error} - ${tokenResponse.error_description}`));
                    } else {
                        console.log('Token acquired.');
                        resolve(tokenResponse);
                    }
                };
                tokenClient.requestAccessToken(options);
            });
        }

        function displayEvents(result) {
            const events = result.items;
            if (events && events.length > 0) {
                let eventList = '<h3>Upcoming Events:</h3><ul>' + events.map(event =>
                    `<li>${event.summary} (${event.start.dateTime || event.start.date})</li>`
                ).join('') + '</ul>';
                document.getElementById('content').innerHTML = eventList;
            } else {
                document.getElementById('content').textContent = 'No upcoming events found.';
            }
        }

        function revokeAccess() {
            const token = gapi.client.getToken();
            if (token && token.access_token) {
                google.accounts.oauth2.revoke(token.access_token, () => {
                    console.log('Access revoked.');
                    document.getElementById('content').textContent = 'Access has been revoked.';
                    gapi.client.setToken(null);
                });
            } else {
                document.getElementById('content').textContent = 'No token to revoke.';
            }
        }
    </script>
</body>
</html>

GAPI 回调

此示例展示了如何使用令牌模型添加 Google Identity 服务库、移除 gapi.auth2 模块,以及使用 JavaScript 版 Google API 客户端库调用 API。

变量用于强制执行库加载顺序。在返回有效的访问令牌后,从回调内部发出 GAPI 调用。

用户应在首次加载页面时按“显示日历”按钮,并在想要刷新日历信息时再次按此按钮。

<!DOCTYPE html>
<html>
<head>
  <script async defer src="https://apis.google.com/js/api.js" onload="gapiLoad()"></script>
  <script async defer src="https://accounts.google.com/gsi/client" onload="gisInit()"></script>
</head>
<body>
  <h1>GAPI with GIS callbacks</h1>
  <button id="showEventsBtn" onclick="showEvents();">Show Calendar</button><br><br>
  <button id="revokeBtn" onclick="revokeToken();">Revoke access token</button>
  <script>
    let tokenClient;
    let gapiInited;
    let gisInited;

    document.getElementById("showEventsBtn").style.visibility="hidden";
    document.getElementById("revokeBtn").style.visibility="hidden";

    function checkBeforeStart() {
       if (gapiInited && gisInited){
          // Start only when both gapi and gis are initialized.
          document.getElementById("showEventsBtn").style.visibility="visible";
          document.getElementById("revokeBtn").style.visibility="visible";
       }
    }

    function gapiInit() {
      gapi.client.init({
        // NOTE: OAuth2 'scope' and 'client_id' parameters have moved to initTokenClient().
      })
      .then(function() {  // Load the Calendar API discovery document.
        gapi.client.load('https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest');
        gapiInited = true;
        checkBeforeStart();
      });
    }

    function gapiLoad() {
        gapi.load('client', gapiInit)
    }

    function gisInit() {
     tokenClient = google.accounts.oauth2.initTokenClient({
                client_id: 'YOUR_CLIENT_ID',
                scope: 'https://www.googleapis.com/auth/calendar.readonly',
                callback: '',  // defined at request time
            });
      gisInited = true;
      checkBeforeStart();
    }

    function showEvents() {

      tokenClient.callback = (resp) => {
        if (resp.error !== undefined) {
          throw(resp);
        }
        // GIS has automatically updated gapi.client with the newly issued access token.
        console.log('gapi.client access token: ' + JSON.stringify(gapi.client.getToken()));

        gapi.client.calendar.events.list({ 'calendarId': 'primary' })
        .then(calendarAPIResponse => console.log(JSON.stringify(calendarAPIResponse)))
        .catch(err => console.log(err));

        document.getElementById("showEventsBtn").innerText = "Refresh Calendar";
      }

      // Conditionally ask users to select the Google Account they'd like to use,
      // and explicitly obtain their consent to fetch their Calendar.
      // NOTE: To request an access token a user gesture is necessary.
      if (gapi.client.getToken() === null) {
        // Prompt the user to select a Google Account and asked for consent to share their data
        // when establishing a new session.
        tokenClient.requestAccessToken({prompt: 'consent'});
      } else {
        // Skip display of account chooser and consent dialog for an existing session.
        tokenClient.requestAccessToken({prompt: ''});
      }
    }

    function revokeToken() {
      let cred = gapi.client.getToken();
      if (cred !== null) {
        google.accounts.oauth2.revoke(cred.access_token, () => {console.log('Revoked: ' + cred.access_token)});
        gapi.client.setToken('');
        document.getElementById("showEventsBtn").innerText = "Show Calendar";
      }
    }
  </script>
</body>
</html>

授权代码流程示例

Google Identity Service 库的弹出式用户体验可以使用网址重定向将授权代码直接返回到后端令牌端点,也可以使用在用户浏览器中运行的 JavaScript 回调处理程序将响应代理到您的平台。无论哪种情况,您的后端平台都会完成 OAuth 2.0 流程,以获取有效的刷新令牌和访问令牌。

旧方法

服务器端 Web 应用

服务器端应用的 Google 登录:在后端平台中运行,通过重定向到 Google 来征求用户同意。

<!DOCTYPE html>
<html>
  <head>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
    <script src="https://apis.google.com/js/client:platform.js?onload=start" async defer></script>
    <script>
      function start() {
        gapi.load('auth2', function() {
          auth2 = gapi.auth2.init({
            client_id: 'YOUR_CLIENT_ID',
            api_key: 'YOUR_API_KEY',
            discovery_docs: ['https://www.googleapis.com/discovery/v1/apis/translate/v2/rest'],
            // Scopes to request in addition to 'profile' and 'email'
            scope: 'https://www.googleapis.com/auth/cloud-translation',
          });
        });
      }
      function signInCallback(authResult) {
        if (authResult['code']) {
          console.log("sending AJAX request");
          // Send authorization code obtained from Google to backend platform
          $.ajax({
            type: 'POST',
            url: 'YOUR_AUTHORIZATION_CODE_ENDPOINT_URL',
            // Always include an X-Requested-With header to protect against CSRF attacks.
            headers: {
              'X-Requested-With': 'XMLHttpRequest'
            },
            contentType: 'application/octet-stream; charset=utf-8',
            success: function(result) {
              console.log(result);
            },
            processData: false,
            data: authResult['code']
          });
        } else {
          console.log('error: failed to obtain authorization code')
        }
      }
    </script>
  </head>
  <body>
    <button id="signinButton">Sign In With Google</button>
    <script>
      $('#signinButton').click(function() {
        // Obtain an authorization code from Google
        auth2.grantOfflineAccess().then(signInCallback);
      });
    </script>
  </body>
</html>

使用重定向的 HTTP/REST

为 Web 服务器应用使用 OAuth 2.0,以将授权代码从用户的浏览器发送到您的后端平台。通过将用户的浏览器重定向到 Google 来处理用户同意情况。

/\*
 \* Create form to request access token from Google's OAuth 2.0 server.
 \*/
function oauthSignIn() {
  // Google's OAuth 2.0 endpoint for requesting an access token
  var oauth2Endpoint = 'https://accounts.google.com/o/oauth2/v2/auth';
  // Create &lt;form> element to submit parameters to OAuth 2.0 endpoint.
  var form = document.createElement('form');
  form.setAttribute('method', 'GET'); // Send as a GET request.
  form.setAttribute('action', oauth2Endpoint);
  // Parameters to pass to OAuth 2.0 endpoint.
  var params = {'client\_id': 'YOUR_CLIENT_ID',
                'redirect\_uri': 'YOUR_AUTHORIZATION_CODE_ENDPOINT_URL',
                'response\_type': 'token',
                'scope': 'https://www.googleapis.com/auth/drive.metadata.readonly',
                'include\_granted\_scopes': 'true',
                'state': 'pass-through value'};
  // Add form parameters as hidden input values.
  for (var p in params) {
    var input = document.createElement('input');
    input.setAttribute('type', 'hidden');
    input.setAttribute('name', p);
    input.setAttribute('value', params[p]);
    form.appendChild(input);
  }

  // Add form to page and submit it to open the OAuth 2.0 endpoint.
  document.body.appendChild(form);
  form.submit();
}

新方式

GIS 弹出式窗口用户体验

此示例仅展示了 Google Identity Service JavaScript 库使用授权代码模型的弹出式对话框,用于征求用户同意,以及用于从 Google 接收授权代码的回调处理程序。它旨在说明配置客户端、征得用户同意以及向后端平台发送授权代码所需的最低步骤数。

<!DOCTYPE html>
<html>
  <head>
    <script src="https://accounts.google.com/gsi/client" onload="initClient()" async defer></script>
  </head>
  <body>
    <script>
      var client;
      function initClient() {
        client = google.accounts.oauth2.initCodeClient({
          client_id: 'YOUR_CLIENT_ID',
          scope: 'https://www.googleapis.com/auth/calendar.readonly',
          ux_mode: 'popup',
          callback: (response) => {
            var code_receiver_uri = 'YOUR_AUTHORIZATION_CODE_ENDPOINT_URI',
            // Send auth code to your backend platform
            const xhr = new XMLHttpRequest();
            xhr.open('POST', code_receiver_uri, true);
            xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
            xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
            xhr.onload = function() {
              console.log('Signed in as: ' + xhr.responseText);
            };
            xhr.send('code=' + response.code);
            // After receipt, the code is exchanged for an access token and
            // refresh token, and the platform then updates this web app
            // running in user's browser with the requested calendar info.
          },
        });
      }
      function getAuthCode() {
        // Request authorization code and obtain user consent
        client.requestCode();
      }
    </script>
    <button onclick="getAuthCode();">Load Your Calendar</button>
  </body>
</html>

GIS 重定向用户体验

授权代码模型支持弹出式窗口和重定向用户体验模式,可将每个用户的授权代码发送到您的平台托管的端点。重定向用户体验模式如下所示:

<!DOCTYPE html>
<html>
  <head>
    <script src="https://accounts.google.com/gsi/client" onload="initClient()" async defer></script>
  </head>
  <body>
    <script>
      var client;
      function initClient() {
        client = google.accounts.oauth2.initCodeClient({
          client_id: 'YOUR_CLIENT_ID',
          scope: 'https://www.googleapis.com/auth/calendar.readonly \
                  https://www.googleapis.com/auth/photoslibrary.readonly',
          ux_mode: 'redirect',
          redirect_uri: 'YOUR_AUTHORIZATION_CODE_ENDPOINT_URI'
        });
      }
      // Request an access token
      function getAuthCode() {
        // Request authorization code and obtain user consent
        client.requestCode();
      }
    </script>
    <button onclick="getAuthCode();">Load Your Calendar</button>
  </body>
</html>

JavaScript 库

Google Identity 服务是一个用于用户身份验证和授权的 JavaScript 库,它整合并取代了多个不同库和模块中的功能:

迁移到 Identity Services 时要执行的操作:

现有 JS 库 新 JS 库 备注
apis.google.com/js/api.js accounts.google.com/gsi/client 添加新库并遵循隐式流程。
apis.google.com/js/client.js accounts.google.com/gsi/client 添加新库和授权码流程。

库快速参考

旧版 Google 登录 JavaScript 客户端库与新版 Google Identity 服务库之间的对象和方法比较,以及备注,其中包含迁移期间需要采取的其他信息和操作。

旧优惠 备注
GoogleAuth 对象和关联方法:
GoogleAuth.attachClickHandler() 移除
GoogleAuth.currentUser.get() 移除
GoogleAuth.currentUser.listen() 移除
GoogleAuth.disconnect() google.accounts.oauth2.revoke 以新换旧。您还可以通过 https://myaccount.google.com/permissions 撤消许可
GoogleAuth.grantOfflineAccess() 移除,请按照授权代码流程操作。
GoogleAuth.isSignedIn.get() 移除
GoogleAuth.isSignedIn.listen() 移除
GoogleAuth.signIn() 移除
GoogleAuth.signOut() 移除
GoogleAuth.then() 移除
GoogleUser 对象和关联的方法:
GoogleUser.disconnect() google.accounts.id.revoke 以新换旧。您还可以通过 https://myaccount.google.com/permissions 撤消许可
GoogleUser.getAuthResponse() requestCode() or requestAccessToken() 将旧内容替换为新内容
GoogleUser.getBasicProfile() 移除。请改用 ID 令牌,参阅从 Google 登录迁移
GoogleUser.getGrantedScopes() hasGrantedAnyScope() 将旧内容替换为新内容
GoogleUser.getHostedDomain() 移除
GoogleUser.getId() 移除
GoogleUser.grantOfflineAccess() 移除,请按照授权代码流程操作。
GoogleUser.grant() 移除
GoogleUser.hasGrantedScopes() hasGrantedAnyScope() 将旧内容替换为新内容
GoogleUser.isSignedIn() 移除
GoogleUser.reloadAuthResponse() requestAccessToken() 移除旧的,调用新的以替换过期或被撤消的访问令牌。
gapi.auth2 对象和关联的方法:
gapi.auth2.AuthorizeConfig 对象 TokenClientConfig 或 CodeClientConfig 将旧内容替换为新内容
gapi.auth2.AuthorizeResponse 对象 移除
gapi.auth2.AuthResponse 对象 移除
gapi.auth2.authorize() requestCode() or requestAccessToken() 将旧内容替换为新内容
gapi.auth2.ClientConfig() TokenClientConfig 或 CodeClientConfig 将旧内容替换为新内容
gapi.auth2.getAuthInstance() 移除
gapi.auth2.init() initTokenClient() or initCodeClient() 将旧内容替换为新内容
gapi.auth2.OfflineAccessOptions 对象 移除
gapi.auth2.SignInOptions 对象 移除
gapi.signin2 对象和关联方法:
gapi.signin2.render() 移除。对 g_id_signin 元素进行 HTML DOM 加载或对 google.accounts.id.renderButton 进行 JS 调用会触发用户登录 Google 账号。

凭据示例

现有凭据

Google 登录平台库适用于 JavaScript 的 Google API 客户端库或对 Google OAuth 2.0 端点的直接调用会在单个响应中同时返回 OAuth 2.0 访问令牌和 OpenID Connect ID 令牌。

包含 access_tokenid_token 的响应示例:

  {
    "token_type": "Bearer",
    "access_token": "ya29.A0ARrdaM-SmArZaCIh68qXsZSzyeU-8mxhQERHrP2EXtxpUuZ-3oW8IW7a6D2J6lRnZrRj8S6-ZcIl5XVEqnqxq5fuMeDDH_6MZgQ5dgP7moY-yTiKR5kdPm-LkuPM-mOtUsylWPd1wpRmvw_AGOZ1UUCa6UD5Hg",
    "scope": "https://www.googleapis.com/auth/calendar.readonly",
    "login_hint": "AJDLj6I2d1RH77cgpe__DdEree1zxHjZJr4Q7yOisoumTZUmo5W2ZmVFHyAomUYzLkrluG-hqt4RnNxrPhArd5y6p8kzO0t8xIfMAe6yhztt6v2E-_Bb4Ec3GLFKikHSXNh5bI-gPrsI",
    "expires_in": 3599,
    "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjkzNDFhYmM0MDkyYjZmYzAzOGU0MDNjOTEwMjJkZDNlNDQ1MzliNTYiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiNTM4MzQ0NjUzMjU1LTc1OGM1aDVpc2M0NXZnazI3ZDhoOGRlYWJvdnBnNnRvLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNTM4MzQ0NjUzMjU1LTc1OGM1aDVpc2M0NXZnazI3ZDhoOGRlYWJvdnBnNnRvLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTE3NzI2NDMxNjUxOTQzNjk4NjAwIiwiaGQiOiJnb29nbGUuY29tIiwiZW1haWwiOiJkYWJyaWFuQGdvb2dsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IkJBSW55TjN2MS1ZejNLQnJUMVo0ckEiLCJuYW1lIjoiQnJpYW4gRGF1Z2hlcnR5IiwicGljdHVyZSI6Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9hLS9BT2gxNEdnenAyTXNGRGZvbVdMX3VDemRYUWNzeVM3ZGtxTE5ybk90S0QzVXNRPXM5Ni1jIiwiZ2l2ZW5fbmFtZSI6IkJyaWFuIiwiZmFtaWx5X25hbWUiOiJEYXVnaGVydHkiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTYzODk5MTYzOCwiZXhwIjoxNjM4OTk1MjM4LCJqdGkiOiI5YmRkZjE1YWFiNzE2ZDhjYmJmNDYwMmM1YWM3YzViN2VhMDQ5OTA5In0.K3EA-3Adw5HA7O8nJVCsX1HmGWxWzYk3P7ViVBb4H4BoT2-HIgxKlx1mi6jSxIUJGEekjw9MC-nL1B9Asgv1vXTMgoGaNna0UoEHYitySI23E5jaMkExkTSLtxI-ih2tJrA2ggfA9Ekj-JFiMc6MuJnwcfBTlsYWRcZOYVw3QpdTZ_VYfhUu-yERAElZCjaAyEXLtVQegRe-ymScra3r9S92TA33ylMb3WDTlfmDpWL0CDdDzby2asXYpl6GQ7SdSj64s49Yw6mdGELZn5WoJqG7Zr2KwIGXJuSxEo-wGbzxNK-mKAiABcFpYP4KHPEUgYyz3n9Vqn2Tfrgp-g65BQ",
    "session_state": {
      "extraQueryParams": {
        "authuser": "0"
      }
    },
    "first_issued_at": 1638991637982,
    "expires_at": 1638995236982,
    "idpId": "google"
  }

Google Identity 服务凭据

Google Identity Services 库会返回:

  • 用于授权时的访问令牌:

    {
      "access_token": "ya29.A0ARrdaM_LWSO-uckLj7IJVNSfnUityT0Xj-UCCrGxFQdxmLiWuAosnAKMVQ2Z0LLqeZdeJii3TgULp6hR_PJxnInBOl8UoUwWoqsrGQ7-swxgy97E8_hnzfhrOWyQBmH6zs0_sUCzwzhEr_FAVqf92sZZHphr0g",
      "token_type": "Bearer",
      "expires_in": 3599,
      "scope": "https://www.googleapis.com/auth/calendar.readonly"
    }
    
  • 或者,在用于身份验证时,为 ID 令牌:

    {
      "clientId": "538344653255-758c5h5isc45vgk27d8h8deabovpg6to.apps.googleusercontent.com",
      "credential": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImMxODkyZWI0OWQ3ZWY5YWRmOGIyZTE0YzA1Y2EwZDAzMjcxNGEyMzciLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJuYmYiOjE2MzkxNTcyNjQsImF1ZCI6IjUzODM0NDY1MzI1NS03NThjNWg1aXNjNDV2Z2syN2Q4aDhkZWFib3ZwZzZ0by5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjExNzcyNjQzMTY1MTk0MzY5ODYwMCIsIm5vbmNlIjoiZm9vYmFyIiwiaGQiOiJnb29nbGUuY29tIiwiZW1haWwiOiJkYWJyaWFuQGdvb2dsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXpwIjoiNTM4MzQ0NjUzMjU1LTc1OGM1aDVpc2M0NXZnazI3ZDhoOGRlYWJvdnBnNnRvLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwibmFtZSI6IkJyaWFuIERhdWdoZXJ0eSIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS0vQU9oMTRHZ3pwMk1zRkRmb21XTF91Q3pkWFFjc3lTN2RrcUxOcm5PdEtEM1VzUT1zOTYtYyIsImdpdmVuX25hbWUiOiJCcmlhbiIsImZhbWlseV9uYW1lIjoiRGF1Z2hlcnR5IiwiaWF0IjoxNjM5MTU3NTY0LCJleHAiOjE2MzkxNjExNjQsImp0aSI6IjRiOTVkYjAyZjU4NDczMmUxZGJkOTY2NWJiMWYzY2VhYzgyMmI0NjUifQ.Cr-AgMsLFeLurnqyGpw0hSomjOCU4S3cU669Hyi4VsbqnAV11zc_z73o6ahe9Nqc26kPVCNRGSqYrDZPfRyTnV6g1PIgc4Zvl-JBuy6O9HhClAK1HhMwh1FpgeYwXqrng1tifmuotuLQnZAiQJM73Gl-J_6s86Buo_1AIx5YAKCucYDUYYdXBIHLxrbALsA5W6pZCqqkMbqpTWteix-G5Q5T8LNsfqIu_uMBUGceqZWFJALhS9ieaDqoxhIqpx_89QAr1YlGu_UO6R6FYl0wDT-nzjyeF5tonSs3FHN0iNIiR3AMOHZu7KUwZaUdHg4eYkU-sQ01QNY_11keHROCRQ",
      "select_by": "user"
    }
    

令牌响应无效

尝试使用过期、已撤消或无效的访问令牌发出 API 请求时,Google 返回的响应示例:

HTTP 响应标头

  www-authenticate: Bearer realm="https://accounts.google.com/", error="invalid_token"

响应正文

  {
    "error": {
      "code": 401,
      "message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
      "errors": [
        {
          "message": "Invalid Credentials",
          "domain": "global",
          "reason": "authError",
          "location": "Authorization",
          "locationType": "header"
        }
      ],
      "status": "UNAUTHENTICATED"
    }
  }