迁移到 Google Identity 服务

<ph type="x-smartling-placeholder">

概览

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

本指南介绍了如何从这些库迁移到 Google Identity Services 库

按照本指南中的说明操作,您将:

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

有关 Identity Services JavaScript 的变更说明 库中的概述用户授权的工作原理, 关键术语和概念。

如果您正在查找用户注册和登录身份验证方法,请参阅 请改为从 Google 登录迁移

确定授权流程

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

查看您的 Web 应用,确定当前的授权流程类型 。

指示您的 Web 应用正在使用隐式流的指示:

如果表明您的 Web 应用正在使用授权代码流程,请执行以下操作:

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

选择授权流程

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

参阅选择授权流程,了解主要区别 并在两个流之间进行权衡取舍。

在大多数情况下,建议您使用授权代码流程,因为它提供了 最高级别的用户安全实施此流程还能让您 更轻松地添加新的离线功能,例如提取更新 通知用户日历、照片、订阅和 依此类推。

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

隐式流

获取一个访问令牌,以便在用户在线时在浏览器内使用。

隐式流程示例显示了迁移到 Google Workspace 迁移前后的 Web 应用 Identity Services。

授权代码流程

向您的后端传送由 Google 发放的每个用户授权代码 然后在该平台上交换访问令牌和刷新令牌。

授权代码流程示例显示了 Web 应用添加前后的对比 迁移到 Identity Services。

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

更改浏览器内 Web 应用

此部分介绍在下列情况下,您可以对浏览器内 Web 应用所做的更改: 迁移到 Google Identity Services JavaScript 库。

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

调试 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 模块用于管理登录和隐式用户身份验证 替换这个已废弃的模块及其对象 方法。

将 Identity Services 库添加到您的 Web 应用,方法是将该库添加到您的 文档:

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

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

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

Cookie

用户授权不要求使用 Cookie。

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

凭据

Google Identity 服务将用户身份验证和授权分开 两种不同的操作,并且用户凭据是独立的: 与用于登录账号的访问令牌分开返回 授权。

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

隐式流

移除用户个人资料,将用户身份验证和授权分开 授权流程的处理

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

方法

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

授权代码流程

Identity Services 将浏览器内凭据拆分为 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() 调用, 按照初始化令牌 客户

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

对象:

  • gapi.auth2.AuthorizeConfigTokenClientConfig 合作

方法:

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

参数:

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

授权代码流程

添加 CodeClientConfig 对象和 initCodeClient() 调用以进行配置 按照初始化代码客户端中的示例操作。

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

移除 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 不支持自动令牌刷新流程 服务库。您的 Web 应用必须更新以检测访问权限过期的情况 并请求一个新令牌如需了解详情,请参阅下面的“令牌处理”部分。

授权代码流程

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

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

令牌处理

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

HTTP 状态代码 401 Unauthorizedinvalid_token 错误消息是 使用过期或撤消的访问令牌时,Google API 会返回相应的值。对于 示例,请参阅令牌响应无效

已过期的令牌

访问令牌只在短时间内有效,通常只有几分钟。

令牌撤消

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 账号登录过程,前提是当前会话确实 已存在。
  • 直接使用 JWT ID 令牌 credential 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() 更新您的 Web 应用, hasGrantedAnyScope()(按照此精细权限示例进行操作)。

授权代码流程

向后端更新添加授权代码端点 按照身份验证代码处理中的说明,向 Google Cloud Platform 集成。

更新您的平台,以遵循使用代码 模型指南,用于验证请求、获取访问令牌和刷新 令牌。

更新您的平台,以选择性地启用或停用相关功能并 基于用户批准的各个范围实现 按照增量授权检查 用户授予的访问权限范围

隐式流示例

旧方法

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 API 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 征求用户意见。

此示例展示了从 用户的浏览器,并且未使用 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 Service 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 异步/等待

此示例展示了如何使用 令牌模型,移除 gapi.auth2 模块,并使用 适用于 JavaScript 的 Google API 客户端库

promise、async 和 await 用于强制执行库加载顺序,并 捕获并重试授权错误。API 调用只有在 访问令牌可用。

用户需要按“显示日历”访问令牌时 首次加载页面时或在访问令牌之后丢失 已过期。

<!DOCTYPE html>
<html>
<head></head>
<body>
  <h1>GAPI with GIS async/await</h1>
  <button id="showEventsBtn" onclick="showEvents();">Show Calendar</button><br><br>
  <button id="revokeBtn" onclick="revokeToken();">Revoke access token</button>

  <script>

    const gapiLoadPromise = new Promise((resolve, reject) => {
      gapiLoadOkay = resolve;
      gapiLoadFail = reject;
    });
    const gisLoadPromise = new Promise((resolve, reject) => {
      gisLoadOkay = resolve;
      gisLoadFail = reject;
    });

    var tokenClient;

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

      // First, load and initialize the gapi.client
      await gapiLoadPromise;
      await new Promise((resolve, reject) => {
        // NOTE: the 'auth2' module is no longer loaded.
        gapi.load('client', {callback: resolve, onerror: reject});
      });
      await 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');
      });

      // Now load the GIS client
      await gisLoadPromise;
      await new Promise((resolve, reject) => {
        try {
          tokenClient = google.accounts.oauth2.initTokenClient({
              client_id: 'YOUR_CLIENT_ID',
              scope: 'https://www.googleapis.com/auth/calendar.readonly',
              prompt: 'consent',
              callback: '',  // defined at request time in await/promise scope.
          });
          resolve();
        } catch (err) {
          reject(err);
        }
      });

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

    async function getToken(err) {

      if (err.result.error.code == 401 || (err.result.error.code == 403) &&
          (err.result.error.status == "PERMISSION_DENIED")) {

        // The access token is missing, invalid, or expired, prompt for user consent to obtain one.
        await new Promise((resolve, reject) => {
          try {
            // Settle this promise in the response callback for requestAccessToken()
            tokenClient.callback = (resp) => {
              if (resp.error !== undefined) {
                reject(resp);
              }
              // GIS has automatically updated gapi.client with the newly issued access token.
              console.log('gapi.client access token: ' + JSON.stringify(gapi.client.getToken()));
              resolve(resp);
            };
            tokenClient.requestAccessToken();
          } catch (err) {
            console.log(err)
          }
        });
      } else {
        // Errors unrelated to authorization: server errors, exceeding quota, bad requests, and so on.
        throw new Error(err);
      }
    }

    function showEvents() {

      // Try to fetch a list of Calendar events. If a valid access token is needed,
      // prompt to obtain one and then retry the original request.
      gapi.client.calendar.events.list({ 'calendarId': 'primary' })
      .then(calendarAPIResponse => console.log(JSON.stringify(calendarAPIResponse)))
      .catch(err  => getToken(err))  // for authorization errors obtain an access token
      .then(retry => gapi.client.calendar.events.list({ 'calendarId': 'primary' }))
      .then(calendarAPIResponse => console.log(JSON.stringify(calendarAPIResponse)))
      .catch(err  => console.log(err)); // cancelled by user, timeout, etc.
    }

    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('');
      }
    }

  </script>

  <script async defer src="https://apis.google.com/js/api.js" onload="gapiLoadOkay()" onerror="gapiLoadFail(event)"></script>
  <script async defer src="https://accounts.google.com/gsi/client" onload="gisLoadOkay()" onerror="gisLoadFail(event)"></script>

</body>
</html>

GAPI 回调

此示例展示了如何使用 令牌模型,移除 gapi.auth2 模块,并使用 适用于 JavaScript 的 Google 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 回调处理程序,在代理 对您的平台做出响应无论是哪种情况,您的后端平台都会 获取有效的刷新和访问令牌。

旧方法

服务器端 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 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 Services 是供用户 旨在整合和替换各种功能的 可在多个不同的库和模块中找到的功能:

迁移到 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 Services 库和 备注:提供更多信息以及在迁移过程中需要采取的行动。

旧优惠 备注
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() 移除。HTML DOM 加载 g_id_signin 元素或 JS 调用 google.accounts.id.renderButton 触发用户登录 Google 账号。

凭据示例

现有凭据

Google 登录平台库适用于 JavaScript 或对 Google Auth 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"
    }
    

令牌响应无效

尝试使用 已过期、已撤消或无效的访问令牌:

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