遷移至 Google Identity 服務

總覽

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

本指南將說明如何從這些程式庫遷移至 Google Identity 服務程式庫

跟著本指南,您將:

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

如需 Identity 服務 JavaScript 程式庫的變更說明,請參閱總覽使用者授權的運作方式,詳閱重要詞彙和概念。

如果您需要使用者註冊和登入的驗證作業,請改為參閱從 Google 登入遷移一文。

找出授權流程

使用者授權流程有兩種,分別是隱含和授權碼。

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

您的網頁應用程式請使用隱含流程

表示網頁應用程式正在使用授權碼流程

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

選擇授權流程

在開始遷移之前,您必須判斷是要繼續使用現有的流程,還是採用其他最符合自身需求的流程。

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

在大多數情況下,我們會建議您使用授權碼流程,因為該流程可提供最高等級的使用者安全。實作此流程也可以讓您的平台更輕鬆地新增離線功能,例如擷取更新以通知使用者有關日曆、相片、訂閱項目等重大變更。

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

隱含流程

取得在使用者存在時在瀏覽器中使用的存取權杖。

隱含流程範例顯示的是遷移至 Identity Services 前後的網頁應用程式。

授權碼流程

系統會將 Google 核發的個別使用者授權碼傳送到您的後端平台,然後用來交換存取權杖和更新權杖。

至於遷移至 Identity 服務前後的網頁應用程式,請見授權碼流程範例

在本指南中,按照粗體列出的說明操作,選擇「新增」、「移除」、「更新」或「取代」現有的功能。

瀏覽器內網頁應用程式異動

本節主要瞭解遷移至 Google Identity 服務 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 Services 程式庫,以便新增 Identity Services 程式庫。

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

憑證

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

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

隱含流程

移除與授權流程中的使用者設定檔處理方式,藉此區隔使用者驗證和授權作業。

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

方法

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

授權碼流程

Identity Services 會將瀏覽器內的憑證分為 ID 權杖和存取權杖。這項變更不適用於藉由您後端平台直接呼叫 Google OAuth 2.0 端點或透過平台安全伺服器上執行的程式庫 (例如 Google APIs 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 Identity 服務取代 Google 登入 JavaScript 用戶端參考資料

物件:

  • gapi.auth2.AuthorizeConfigTokenClientConfig 合作

方法:

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

參數:

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

授權碼流程

新增 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 權杖流程
  • 使用 requestAccessTokenOverridableTokenClientConfig,將多個範圍的要求分割成多個較小的要求,即可支援漸進式授權。
  • 請在現有權杖過期或撤銷時要求新權杖。

使用多個範圍時,可能需要變更程式碼集的結構,以要求僅有需要的範圍存取權時才能要求存取權,而非一次要求所有範圍,這稱為漸進式授權。每個要求包含的範圍都應盡可能少,最好只有單一範圍。如要進一步瞭解如何更新應用程式以進行增量授權,請參閱「如何處理使用者同意聲明」。

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

授權碼流程

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

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

權杖處理

新增錯誤處理機制,當使用過期或撤銷的存取權杖時,偵測失敗的 Google API 呼叫,以及要求新的有效存取權杖。

如果存取權杖已過期或遭到撤銷,Google API 會傳回 HTTP 狀態碼 401 Unauthorizedinvalid_token 錯誤訊息。如需範例,請參閱無效的權杖回應

權杖已過期

存取權杖是暫時性的,通常僅會在幾分鐘內有效。

撤銷權杖

Google 帳戶擁有者可隨時撤銷先前所授予的同意聲明。這樣會導致現有的存取權杖和更新權杖失效。您的平台可以使用 revoke()Google 帳戶來觸發撤銷作業。

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

方法:

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

當使用者在您的平台上刪除他們的帳戶時,請呼叫 revoke,或確認同意與應用程式共用資料。

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

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

使用者登入

使用者可以在另一個瀏覽器分頁中登入 Google 帳戶,也可以直接透過瀏覽器或作業系統登入 Google 帳戶。建議您在網站中新增「使用 Google 帳戶登入」功能,以便在使用者首次開啟應用程式時,在 Google 帳戶和瀏覽器之間建立有效工作階段。這提供以下優點:

  • 盡量減少使用者登入必須登入的次數。如果當下還沒有執行中的工作階段,則要求存取權杖會啟動 Google 帳戶登入程序。
  • 直接使用 JWT ID 憑證「credential」email 欄位做為 CodeClientConfigTokenClientConfig 物件中的 login_hint 參數值。如果您的平台沒有維護使用者帳戶管理系統,這項功能就特別實用。
  • 查詢 Google 帳戶並連結至平台上現有的本機使用者帳戶,盡可能減少平台上的重複帳戶。
  • 建立新的本機帳戶時,註冊對話方塊和流程可以清楚區隔使用者驗證對話方塊和流程,進而減少必要步驟,並提升流失率。

登入後,發出存取權杖前,使用者必須針對所要求的範圍為應用程式提供同意聲明。

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

精細的權限可讓使用者核准或拒絕個別範圍。要求多個範圍的存取權時,每個範圍都會獲得或拒絕,不受其他範圍影響。您可以根據使用者選擇,選擇啟用需要用到個別範圍的功能。

隱含流程

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

物件:

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

方法:

  • GoogleUser.hasGrantedScopes() 搭配 google.accounts.oauth2.hasGrantedAllScopes()
  • GoogleUser.getGrantedScopes() 搭配 google.accounts.oauth2.hasGrantedAllScopes()

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

方法:

  • GoogleUser.getAuthResponse()

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

授權碼流程

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

更新平台,按照使用程式碼範例指南中的步驟驗證要求,並取得存取權杖及更新權杖。

按照增量授權檢視使用者授予的存取權範圍相關說明,更新平台,依據使用者核准的個別範圍,選擇性啟用或停用功能。

隱含流程範例

舊式

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

針對用戶端網頁應用程式適用的 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 async/await

這個範例說明如何使用權杖模型新增 Google Identity 服務程式庫、移除 gapi.auth2 模組,以及使用 JavaScript 適用的 Google API 用戶端程式庫呼叫 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 回呼

這個範例說明如何使用權杖模型新增 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 Services 時應採取的行動:

現有的 JS 程式庫 新增 JS 程式庫 附註
apis.google.com/js/api.js accounts.google.com/gsi/client 新增程式庫,並遵循隱含流程。
apis.google.com/js/client.js accounts.google.com/gsi/client 新增程式庫和授權碼流程。

程式庫快速參考資料

「舊版」Google 登入 JavaScript 用戶端程式庫與新的 Google Identity 服務程式庫與附註之間的物件和方法比較,並附上額外資訊和遷移期間要採取的行動。

舊優惠 新功能 附註
GoogleAuth 物件和相關聯的方法:
GoogleAuth.attachClickHandler() 移除
GoogleAuth.currentUser.get() 移除
GoogleAuth.currentUser.listen() 移除
GoogleAuth.disconnect() google.accounts.oauth2.revoke 以新取代。您也可以前往 https://myaccount.google.com/permissions 撤銷授權
GoogleAuth.grantOfflineAccess() 移除授權碼,請按照授權碼流程操作。
GoogleAuth.isSignedIn.get() 移除
GoogleAuth.isSignedIn.listen() 移除
GoogleAuth.signIn() 移除
GoogleAuth.signOut() 移除
GoogleAuth.then() 移除
GoogleUser 物件和相關方法:
GoogleUser.disconnect() google.accounts.id.revoke 以新取代。您也可以前往 https://myaccount.google.com/permissions 撤銷授權
GoogleUser.getAuthResponse() requestCode() or requestAccessToken() 以新取代
GoogleUser.getBasicProfile() ,即可逐一移除離線觀看清單內的影片;請改為使用 ID 權杖,請參閱從 Google 登入遷移
GoogleUser.getGrantedScopes() hasGrantedAnyScope() 以新取代
GoogleUser.getHostedDomain() 移除
GoogleUser.getId() 移除
GoogleUser.grantOfflineAccess() 移除授權碼,請按照授權碼流程操作。
GoogleUser.grant() 移除
GoogleUser.hasGrantedScopes() hasGrantedAnyScope() 以新取代
GoogleUser.isSignedIn() 移除
GoogleUser.reloadAuthResponse() requestAccessToken() 請移除舊憑證,並呼叫新的憑證來取代過期或撤銷的存取權。
gapi.auth2 物件和相關方法:
gapi.auth2.AuthorizeConfig 物件 TokenClientConfig 或 CodeClientConfig 以新取代
gapi.auth2.AuthorizeResponse 物件 移除
gapi.auth2.AuthResponse 物件 移除
gapi.auth2.authorize() requestCode() or requestAccessToken() 以新取代
gapi.auth2.ClientConfig() TokenClientConfig 或 CodeClientConfig 以新取代
gapi.auth2.getAuthInstance() 移除
gapi.auth2.init() initTokenClient() or initCodeClient() 以新取代
gapi.auth2.OfflineAccessOptions 物件 移除
gapi.auth2.SignInOptions 物件 移除
gapi.signin2 物件和相關方法:
gapi.signin2.render() ,即可逐一移除離線觀看清單內的影片;g_id_signin 元素載入的 HTML DOM 或是向 google.accounts.id.renderButton 發出 JS 呼叫,都會觸發使用者登入 Google 帳戶的程序。

範例憑證

現有憑證

Google 登入平台程式庫JavaScript 適用的 Google API 用戶端程式庫,或直接呼叫 Google 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"
    }
    

權杖回應無效

Google 嘗試使用過期、已撤銷或無效的存取權杖發出 API 要求時,回應範例:

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