迁移到 Google Identity 服务

概览

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

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

通过遵循本指南,您将:

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

如需了解身份服务 JavaScript 库的变化,请参阅概览和用户授权的工作原理,以查看关键术语和概念。

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

确定授权流程

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

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

您的 Web 应用使用隐式流的迹象:

以下迹象表明您的 Web 应用使用的是授权代码流程:

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

选择授权流程

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

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

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

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

隐式流

在用户在场时获取访问令牌,以便在浏览器中使用。

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

授权代码流程

Google 颁发的每用户授权代码会传送到您的后端平台,然后在此处交换为访问令牌和刷新令牌。

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

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

对浏览器内 Web 应用的更改

本部分回顾了在迁移到 Google Identity 服务 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 模块。您可以放心地继续使用 JavaScript 版 Google API 客户端库中的 gapi.client 模块,并利用其从发现文档自动创建可调用 JS 方法、批量处理多个 API 调用以及 CORS 管理功能。

Cookie

用户授权无需使用 Cookie。

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

凭证

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

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

隐式流

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

移除以下 Google 登录 JavaScript 客户端引用:

方法

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

授权代码流程

身份服务将浏览器内凭据分为 ID 令牌和访问令牌。此变更不适用于通过从后端平台直接调用 Google OAuth 2.0 端点或通过在平台上的安全服务器上运行的库(例如 Google API Node.js 客户端)获取的凭据。

会话状态

以前,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.AuthorizeConfig 与 TokenClientConfig 合作

方法:

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

参数:

  • gapi.auth2.AuthorizeConfig.login_hint 是 TokenClientConfig.login_hint。
  • 使用 TokenClientConfig.hd 调用 gapi.auth2.GoogleUser.getHostedDomain()。

授权代码流程

按照初始化 Code Client 中的示例,添加 CodeClientConfig 对象和 initCodeClient() 调用,以配置您的 Web 应用。

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

移除 Google 登录 JavaScript 客户端引用

对象:

  • gapi.auth2.AuthorizeConfig

方法:

  • gapi.auth2.init()

参数:

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

令牌请求

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

隐式流

当用户登录并与 Google 建立有效会话时,可以在浏览器中获取和使用访问令牌。对于隐式模式,即使之前发出过请求,也需要用户手势才能请求访问令牌。

将 Google 登录 JavaScript 客户端引用替换为 Google Identity 服务:

方法:

  • gapi.auth2.authorize() 与 TokenClient.requestAccessToken() 合作
  • GoogleUser.reloadAuthResponse()(包含TokenClient.requestAccessToken())

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

将代码库更新为:

  • 使用 requestAccessToken() 触发 OAuth 2.0 令牌流程。
  • 使用 requestAccessToken 和 OverridableTokenClientConfig 将一个包含多个范围的请求拆分为多个较小的请求,从而支持增量授权。
  • 在现有令牌过期或被撤消时,请求新令牌。

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

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

授权代码流程

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

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

令牌处理

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

当使用过期或已撤消的访问令牌时,Google API 会返回 HTTP 状态代码 401 Unauthorized 和 invalid_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 账号登录流程。
  • 直接使用 JWT ID 令牌凭证 email 字段作为 CodeClientConfig 或 TokenClientConfig 对象中 login_hint 参数的值。如果您的平台不维护用户账号管理系统,此功能会特别有用。
  • 查找 Google 账号并将其与您平台上的现有本地用户账号相关联,从而最大限度地减少您平台上的重复账号。
  • 创建新的本地账号时,您的注册对话框和流程可以与用户身份验证对话框和流程明确分开,从而减少所需步骤并提高访问者流失率。

登录后,在系统颁发访问令牌之前,用户必须向您的应用授予所请求范围的权限。

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

精细权限允许用户批准或拒绝个别范围。请求访问多个范围时,每个范围的授予或拒绝都独立于其他范围。根据用户选择,您的应用会选择性地启用依赖于单个范围的功能。

隐式流

将 Google 登录 JavaScript 客户端引用替换为Google Identity 服务:

对象:

  • gapi.auth2.AuthorizeResponse 与 TokenClient.TokenResponse 合作
  • gapi.auth2.AuthResponse 与 TokenClient.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 the Google authorization 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 the Google authorization 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 服务库弹出式用户体验可以使用网址重定向将授权代码直接返回到后端令牌端点,也可以使用在用户浏览器中运行的 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

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

/*
 * Create form to request access token from the Google authorization 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 服务 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 库,它整合并取代了多个不同库和模块中的特性和功能:

迁移到身份服务时要执行的操作:

现有 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() 或 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() 或 requestAccessToken() 以旧换新
gapi.auth2.ClientConfig() TokenClientConfig 或 CodeClientConfig 以旧换新
gapi.auth2.getAuthInstance() 移除
gapi.auth2.init() initTokenClient() 或 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_token 和 id_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 服务库会返回:

  • 用于授权的访问令牌:

    {
      "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"
    }
  }