遷移至 Google Identity 服務

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

總覽

為了取得每個使用者呼叫權杖以呼叫 Google API,Google 提供多個 JavaScript 程式庫:

本指南提供從這些程式庫遷移至 Google Identity 服務程式庫的操作說明。

只要遵循這份指南,您將:

  • 將已淘汰的平台程式庫替換為 Identity Services Library。
  • 如果您使用 API 用戶端程式庫,請移除已淘汰的 gapi.auth2 模組及其方法和物件,並以 Identity Service 的對等項目取代。

如需 Identity Services JavaScript 程式庫異動的內容,請參閱總覽使用者授權的運作方式,瞭解重要詞彙和概念。

如果您需要使用者註冊和登入驗證功能,請改為參閱從 Google 登入遷移

識別您的授權流程

使用者授權流程有兩種:隱式和授權碼。

檢查您的網路應用程式,找出目前使用的授權流程類型。

您的網路應用程式使用的隱含流程

您的網頁應用程式使用的是授權碼流程

在某些情況下,您的程式碼集可能支援這兩種流程。

選擇授權流程

開始遷移之前,您必須決定要繼續使用現有流程還是採用不同的流程。

請參閱選擇授權流程來瞭解兩段流程之間的主要差異和優缺點。

在大部分情況下,我們會建議提供授權碼流程,因為這個機制會提供最高等級的使用者安全性。實作這個流程還能讓您的平台更輕鬆地新增離線功能,例如擷取更新,通知使用者日曆、相片、訂閱項目等的重要異動。

使用下方選取器選擇授權流程。

隱含流程

取得使用者需要的存取權杖,以供在瀏覽器內使用。

隱含流程範例顯示網路應用程式遷移至 Identity 服務之前和之後的情境。

授權碼流程

Google 將每個使用者核發的授權碼傳送至您的後端平台,然後在該平台上交換存取權杖和更新權杖。

授權碼流程範例顯示遷移至移轉服務之前與之後的網路應用程式。

在本指南中,請按照粗體標示的操作說明進行「新增」、「移除」、「更新」或「取代」現有功能。

瀏覽器網路瀏覽器中的變更

本節將說明您在遷移至 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 模組會管理登入作業的使用者驗證,以及用於授權的隱含流程,請將這個已淘汰的模組及其物件和方法替換為 Google Identity 服務程式庫。

在文件中加入 Identity 服務程式庫,以在文件中新增 Identity 程式庫:

<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 如何使用 Cookie 用於其他 Google 產品和服務的 Cookie,請參閱從 Google 登入遷移一節。

憑證

Google Identity 服務將使用者驗證和授權分為兩個不同的作業,而使用者憑證則各自獨立:用來識別使用者的 ID 權杖會與用於授權的存取權杖分開傳回。

如要查看這些變更,請參閱憑證範例

隱含流程

從授權流程中移除使用者個人資料處理,將使用者驗證和授權分隔開來。

移除這些 Google 登入 JavaScript 用戶端參考資料

方法

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

授權碼流程

Identity 服務會將瀏覽器內的憑證分為 ID 憑證及存取憑證。這項變更不適用於透過後端平台直接呼叫 Google OAuth 2.0 端點,或利用您平台上的安全伺服器上執行的程式庫 (例如 Google API Node.js 用戶端) 取得的憑證。

工作階段狀態

先前,Google 登入服務可利用以下功能,協助您管理使用者登入狀態:

您必須負責管理您網頁應用程式的登入狀態和使用者工作階段。

移除這些 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()

用戶端設定

更新網頁應用程式,以初始化隱含或授權程式碼流程中的權杖用戶端。

移除這些 Google 登入 JavaScript 用戶端參考資料

物件:

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

方法:

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

隱含流程

按照初始化權杖用戶端中的範例,新增 TokenClientConfig 物件和 initTokenClient() 呼叫以設定網頁應用程式。

Google 登入 JavaScript 用戶端參考資料替換為 Google Identity 服務

物件:

  • 附有TokenClientConfiggapi.auth2.AuthorizeConfig

方法:

  • 附有google.accounts.oauth2.initTokenClient()gapi.auth2.init()

參數:

  • gapi.auth2.AuthorizeConfig.login_hint (使用TokenClientConfig.hint)。
  • gapi.auth2.GoogleUser.getHostedDomain() (使用TokenClientConfig.hosted_domain)。

授權碼流程

按照初始化程式碼用戶端中的範例,新增 CodeClientConfig 物件和 initCodeClient() 呼叫以設定您的網頁應用程式。

從隱含代碼切換至授權碼流程時:

移除 Google 登入 JavaScript 用戶端參考資料

物件:

  • gapi.auth2.AuthorizeConfig

方法:

  • gapi.auth2.init()

參數:

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

憑證要求

使用者手勢 (例如按下按鈕) 會產生請求,導致以隱含流程直接傳回使用者權杖,或者在將存取權杖和更新權杖交換個別使用者代碼之後,將後端權杖傳回您的後端平台。

隱含流程

當使用者登入時,只要在瀏覽器中登入 Google 並登入 Google,就能取得存取憑證。如果是隱含模式,則需要使用者手勢才能要求存取權杖,即使之前發生過要求也沒問題。

取代 Google 登入 JavaScript 用戶端參考資料Google Identity 服務

方法:

  • 附有TokenClient.requestAccessToken()gapi.auth2.authorize()
  • 附有TokenClient.requestAccessToken()GoogleUser.reloadAuthResponse()

新增連結或按鈕,呼叫 requestAccessToken() 以啟動彈出式視窗的使用者體驗流程來要求存取權杖,或在現有權杖過期時取得新的權杖。

將程式碼集更新至:

  • 使用 requestAccessToken() 觸發 OAuth 2.0 憑證流程
  • 使用 requestAccessTokenOverridableTokenClientConfig 支援針對多個範圍將一項要求拆分為多個小型要求,藉此支援漸進式授權。
  • 在現有憑證過期或遭到撤銷時要求新憑證。

使用多個範圍時,可能需要對程式碼集進行結構變更,以便僅要求所需範圍,而不是一次存取所有範圍,這稱為漸進式授權。每個要求都應含有較少範圍,最好是單一範圍。如要進一步瞭解如何更新應用程式以取得漸進式授權,請參閱如何處理使用者同意聲明

存取權杖到期後,gapi.auth2 模組會自動為您的網頁應用程式取得新的有效存取權杖。為了提高使用者安全性,Google Identity 服務程式庫不支援這項自動憑證重新整理程序。您必須更新網頁應用程式,才能偵測過期的存取憑證並要求新的權杖。詳情請參閱下方的「權杖處理」一節。

授權碼流程

新增連結或按鈕,呼叫 requestCode() 要求 Google 提供授權碼。如需範例,請參閱觸發 OAuth 2.0 程式碼流程一文。

如要進一步瞭解如何回應過期或撤銷的存取憑證,請參閱下方的「權杖處理」一節。

符記處理

使用 Add 錯誤處理方式,以便在使用過期或遭撤銷的存取權杖時偵測失敗的 Google API 呼叫,並要求新的有效存取權杖。

使用過期或撤銷的存取憑證時,Google API 會傳回 401 Unauthorizedinvalid_token 的 HTTP 狀態碼。如需範例,請參閱權杖回應無效

過期的符記

存取憑證較短,而且通常只有幾分鐘有效。

憑證撤銷

Google 帳戶擁有者可隨時撤銷先前同意的同意聲明。 這樣做會讓現有的存取憑證和更新憑證失效。您可以透過 revoke()Google 帳戶從平台觸發撤銷作業。

取代 Google 登入 JavaScript 用戶端參考資料Google Identity 服務

方法:

  • 附有google.accounts.oauth2.revoke()getAuthInstance().disconnect()
  • 附有google.accounts.oauth2.revoke()GoogleUser.disconnect()

當使用者刪除平台上的帳戶時,請呼叫 revoke,或要移除與應用程式共用資料的同意聲明。

當網頁應用程式或後端平台要求存取權杖時,Google 會向使用者顯示同意聲明對話方塊。請參閱 Google 向使用者顯示的同意聲明對話方塊範例。

在向應用程式核發存取權杖之前,您必須先使用現有的有效 Google 工作階段,才能提示使用者同意並記錄結果。如果現有的工作階段尚未建立,則使用者可能必須登入 Google 帳戶。

使用者登入

使用者可以在其他瀏覽器分頁中登入 Google 帳戶,或是透過瀏覽器或作業系統以原生方式登入。我們建議您在網站中加入登入 Google功能,以便在使用者首次開啟應用程式時,在 Google 帳戶和瀏覽器之間建立有效的工作階段。這麼做的好處如下:

  • 盡量減少使用者登入的次數;如果存取工作階段不存在,則要求存取權杖會啟動 Google 帳戶登入程序。
  • 直接使用 JWT ID 憑證憑證 email 欄位做為 CodeClientConfigTokenClientConfig 物件中 hint 參數值。如果您的平台沒有維護使用者帳戶管理系統,這種做法就特別實用。
  • 在平台上查詢 Google 帳戶與現有的本機使用者帳戶並建立關聯,協助減少平台上的重複帳戶。
  • 建立新的本機帳戶時,您的註冊對話方塊和流程可以明確地與使用者驗證對話方塊和流程分開,減少必要步驟的數量,並改善放棄率。

登入後,在核發存取憑證前,使用者必須針對要求的範圍提供同意聲明。

取得使用者同意後,系統會傳回存取權杖,以及使用者核准或拒絕的範圍清單。

精細權限可讓使用者核准或拒絕個別範圍。要求存取多個範圍時,無論其他範圍為何,系統都會授予或拒絕每個範圍。根據使用者的選擇,您的應用程式會選擇性啟用取決於個別範圍的功能。

隱含流程

Google 登入 JavaScript 用戶端參考資料替換為 Google Identity 服務

物件:

  • 附有TokenClient.TokenResponsegapi.auth2.AuthorizeResponse
  • 附有TokenClient.TokenResponsegapi.auth2.AuthResponse

方法:

  • 附有google.accounts.oauth2.hasGrantedAllScopes()GoogleUser.hasGrantedScopes()
  • 附有google.accounts.oauth2.hasGrantedAllScopes()GoogleUser.getGrantedScopes()

移除 Google 登入 JavaScript 用戶端參考資料

方法:

  • GoogleUser.getAuthResponse()

按照以下精細權限範例,使用 hasGrantedAllScopes()hasGrantedAnyScope() 更新網頁應用程式。

授權碼流程

按照驗證碼處理中的操作說明,更新新增至後端平台的授權碼端點。

更新平台,按照使用程式碼模型指南所述的步驟來驗證要求並取得存取權杖和重新整理權杖。

按照漸進式授權檢查使用者授予存取權範圍的操作說明,更新平台,根據使用者核准的個別範圍,選擇啟用或停用這些功能。

隱含流程範例

GAPI 用戶端程式庫

瀏覽器專用 Google API 用戶端程式庫範例,內有利用使用者同意聲明的彈出式對話方塊,在瀏覽器中執行。

gapi.client.init() 會自動載入並使用 gapi.auth2 模組,因此已隱藏。

<!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 用戶端程式庫

用戶端網路應用程式的 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 端點

用戶端專用網路應用程式的 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 非同步/等待

此範例說明如何使用權杖模型新增 Google Identity 服務程式庫、移除 gapi.auth2 模組,以及使用 JavaScript 適用的 Google API 用戶端程式庫來呼叫 API。

承諾、非同步和等待將用於強制執行程式庫載入順序,並擷取及重試授權錯誤。只有在有效存取權杖可用時,系統才會發出 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 回呼

此範例說明如何使用權杖模型新增 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 回呼處理常式,透過 Proxy 將回應回應您的平台。無論是哪一種情況,您的後端平台都會完成 OAuth 2.0 流程,以取得有效的重新整理和存取權杖。

伺服器端網路應用程式

在伺服器端平台執行的 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 服務 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 服務時必須採取的行動:

現有的 JS 程式庫 新的 JS 程式庫 Notes
apis.google.com/js/api.js accounts.google.com/gsi/client 新增程式庫,並按照隱含流程操作。
apis.google.com/js/client.js accounts.google.com/gsi/client 新增程式庫和授權碼流程。

程式庫快速參考指引

Old Google 登入 JavaScript 用戶端資料庫與新版 Google Identity Services 程式庫與附註之間的物件與方法比較,其中在遷移過程中會參考其他資訊及動作。

舊優惠 全新內容 Notes
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.getGrantedScopeScopes() 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.authorization() 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 或向 google.accounts.id.renderButton 呼叫 JS 的 HTML DOM 觸發條件會觸發使用者登入 Google 帳戶的狀態。

憑證範例

現有憑證

Google 登入平台程式庫JavaScript 專用的 Google API 用戶端程式庫,或直接呼叫 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 服務程式庫會傳回:

  • 用於授權的存取權杖:
  {
    "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"
    }
  }