遷移至 Google Identity 服務

總覽

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

本指南說明如何從這些程式庫遷移至 Google Identity Services 程式庫。

按照本指南操作,您將能:

  • 將已淘汰的平台程式庫替換為身分識別服務程式庫,
  • 如果使用 API 用戶端程式庫,請移除已淘汰的 gapi.auth2 模組、其方法和物件,並替換為身分識別服務的對等項目。

如要瞭解身分識別服務 JavaScript 程式庫的異動內容,請參閱總覽和使用者授權運作方式,查看重要字詞和概念。

如要瞭解使用者註冊和登入的驗證方式,請改為參閱「從 Google 登入遷移」。

找出授權流程

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

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

網頁應用程式使用隱含流程的跡象:

如果網頁應用程式使用授權碼流程,會出現以下跡象:

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

選擇授權流程

開始遷移前,您必須判斷繼續使用現有流程或採用其他流程,是否最符合您的需求。

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

在大多數情況下,建議使用授權碼流程,因為這能提供最高層級的使用者安全性。實作這個流程後,平台也能新增離線功能,例如擷取更新內容,通知使用者日曆、相片和訂閱項目有重大異動。

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

隱含流程

在使用者在場時,取得瀏覽器內使用的存取權杖。

隱含流程範例顯示遷移至身分識別服務前後的網頁應用程式。

授權碼流程

Google 核發的每位使用者授權碼會傳送至後端平台,然後換成存取權杖和更新權杖。

授權碼流程範例顯示網頁應用程式遷移至身分識別服務前後的差異。

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

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

本節將說明遷移至 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 程式庫,新增至網頁應用程式:

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

移除所有使用 gapi.load('auth2', function) 載入 auth2 模組的例項。

Google Identity 服務程式庫會取代 gapi.auth2 模組。您可以放心繼續使用 JavaScript 專用 Google API 用戶端程式庫中的 gapi.client 模組,並利用該模組從導覽文件自動建立可呼叫的 JS 方法、批次處理多個 API 呼叫,以及 CORS 管理功能。

Cookie

使用者授權不需要使用 Cookie。

如要瞭解使用者驗證如何使用 Cookie,請參閱「從 Google 登入遷移」;如要瞭解其他 Google 產品和服務如何使用 Cookie,請參閱「Google 如何使用 Cookie」。

憑證

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

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

隱含流程

從授權流程中移除使用者個人資料處理,藉此區分使用者驗證和授權。

移除下列 Google 登入 JavaScript 用戶端參照:

方法

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

授權碼流程

身分識別服務會將瀏覽器內憑證分為 ID 權杖和存取權杖。這項異動不適用於透過後端平台直接呼叫 Google OAuth 2.0 端點,或透過在平台安全伺服器上執行的程式庫 (例如 Google API Node.js 用戶端) 取得的憑證。

工作階段狀態

過去,Google 登入功能會使用下列項目,協助您管理使用者登入狀態:

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

移除下列 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 Services:

物件:

  • gapi.auth2.AuthorizeConfig (與TokenClientConfig合作)

方法:

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

參數:

  • gapi.auth2.AuthorizeConfig.login_hint與TokenClientConfig.login_hint。
  • 使用「TokenClientConfig.hd」gapi.auth2.GoogleUser.getHostedDomain()。

授權碼流程

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

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

移除 Google 登入 JavaScript 用戶端參照

物件:

  • gapi.auth2.AuthorizeConfig

方法:

  • gapi.auth2.init()

參數:

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

權杖要求

使用者手勢 (例如點選按鈕) 會產生要求,系統會透過隱含流程,將存取權杖直接傳回使用者的瀏覽器,或將每位使用者的授權碼換成存取權杖和更新權杖,然後傳回您的後端平台。

隱含流程

使用者登入 Google 並處於有效工作階段時,可在瀏覽器中取得及使用存取權杖。在隱含模式下,即使先前已提出要求,仍須透過使用者手勢要求存取權杖。

將 Google 登入 JavaScript 用戶端參照替換為 Google Identity Services:

方法:

  • gapi.auth2.authorize() (與TokenClient.requestAccessToken()合作)
  • GoogleUser.reloadAuthResponse() (搭配 TokenClient.requestAccessToken())

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

將程式碼集更新為:

  • 使用 requestAccessToken() 觸發 OAuth 2.0 權杖流程。
  • 使用 requestAccessToken 和 OverridableTokenClientConfig,將一個要求拆分為多個較小的要求,以支援漸進式授權。
  • 現有權杖過期或撤銷時,請要求新的權杖。

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

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

授權碼流程

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

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

權杖處理

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

使用過期或撤銷的存取權杖時,Google API 會傳回 401 Unauthorized 的 HTTP 狀態碼和 invalid_token 錯誤訊息。如需範例,請參閱「無效權杖回應」。

過期的權杖

存取權杖的效期很短,通常只有幾分鐘。

撤銷權杖

Google 帳戶擁有者隨時可以撤銷先前授予的同意聲明。這麼做會使現有的存取權杖和更新權杖失效。撤銷作業可透過 revoke() 從平台觸發,或透過 Google 帳戶觸發。

將 Google 登入 JavaScript 用戶端參照替換為 Google Identity Services:

方法:

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

當使用者在您的平台上刪除帳戶,或希望撤銷與應用程式共用資料的同意聲明時,請呼叫 revoke。

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

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

使用者登入

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

  • 盡可能減少使用者必須登入的次數,如果沒有有效的工作階段,要求存取權杖會啟動 Google 帳戶登入程序。
  • 直接使用 JWT ID 權杖憑證 email 欄位,做為 CodeClientConfig 或 TokenClientConfig 物件中 login_hint 參數的值。如果平台未維護使用者帳戶管理系統,這項功能就特別實用。
  • 查詢 Google 帳戶,並與平台上的現有本機使用者帳戶建立關聯,盡可能減少平台上的重複帳戶。
  • 建立新的本機帳戶時,註冊對話方塊和流程可以與使用者驗證對話方塊和流程明確區隔,減少必要步驟並改善訪客流失率。

登入後,在核發存取權杖前,使用者必須同意應用程式存取要求的範圍。

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

精細權限可讓使用者核准或拒絕個別範圍。要求存取多個範圍時,每個範圍都會獨立授予或拒絕,不受其他範圍影響。根據使用者選擇,應用程式會選擇性啟用取決於個別範圍的功能。

隱含流程

將 Google 登入 JavaScript 用戶端參照替換為 Google Identity Services:

物件:

  • gapi.auth2.AuthorizeResponse (與TokenClient.TokenResponse合作)
  • gapi.auth2.AuthResponse (與TokenClient.TokenResponse合作)

方法:

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

移除 Google 登入 JavaScript 用戶端參照:

方法:

  • GoogleUser.getAuthResponse()

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

授權碼流程

按照授權碼處理一節的指示,在後端平台更新或新增授權碼端點。

更新平台,按照「使用程式碼模型」指南中的步驟驗證要求,並取得存取權杖和更新權杖。

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

隱含流程範例

傳統做法

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 Google Cloud console.
    // 'scope' field specifies space-delimited list of access scopes.
    gapi.client.init({
        'apiKey': 'YOUR_API_KEY',
        'clientId': 'YOUR_CLIENT_ID',
        'discoveryDocs': [discoveryUrl],
        'scope': SCOPE
    }).then(function () {
      GoogleAuth = gapi.auth2.getAuthInstance();

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

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

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

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

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

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

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

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

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

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

OAuth 2.0 端點

OAuth 2.0 for 用戶端 網頁應用程式:在瀏覽器中執行,並使用重新導向功能,將使用者導向 Google 授權伺服器,徵求使用者同意。

這個範例顯示從使用者瀏覽器直接呼叫 Google 的 OAuth 2.0 端點,不使用 gapi.auth2 模組或 JavaScript 程式庫。

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

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

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

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

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

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

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

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

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

新做法

僅限 GIS

這個範例僅顯示使用權杖模型的 Google Identity 服務 JavaScript 程式庫,以及使用者同意聲明的彈出式對話方塊。這份指南旨在說明設定用戶端、要求及取得存取權杖,以及呼叫 Google API 所需的最少步驟。

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

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

GAPI async/await

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

Promise、async 和 await 可用來強制執行程式庫載入順序,以及擷取和重試授權錯誤。只有在取得有效的存取權杖後,系統才會發出 API 呼叫。

如果網頁首次載入時沒有存取權杖,或存取權杖過期,使用者應按下「Show Calendar」按鈕。

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

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

        let tokenClient;
        let libsLoaded = 0;

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

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

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

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

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

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

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

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

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

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

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

GAPI 回呼

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

變數可用來強制執行程式庫載入順序。傳回有效的存取權杖後,系統會從回呼中發出 GAPI 呼叫。

使用者應在網頁首次載入時按下「顯示日曆」按鈕,並在想重新整理日曆資訊時再次按下。

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

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

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

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

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

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

    function showEvents() {

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

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

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

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

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

授權碼流程範例

Google Identity 服務程式庫的彈出式視窗使用者體驗,可使用網址重新導向,直接將授權碼傳回後端權杖端點,或使用在使用者瀏覽器中執行的 JavaScript 回呼處理常式,將回應代理至平台。無論是哪種情況,後端平台都會完成 OAuth 2.0 流程,取得有效的更新和存取權杖。

傳統做法

伺服器端網頁應用程式

伺服器端應用程式的 Google 登入:在後端平台執行,並重新導向至 Google 取得使用者同意。

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

使用重新導向的 HTTP/REST

將 OAuth 2.0 用於網路伺服器應用程式,將授權碼從使用者瀏覽器傳送至後端平台。將使用者的瀏覽器重新導向至 Google 授權伺服器,以處理使用者同意聲明。

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

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

新做法

GIS 彈出式視窗使用者體驗

這個範例僅顯示使用授權碼模式的 Google Identity 服務 JavaScript 程式庫,用於徵求使用者同意聲明的彈出式對話方塊,以及用於接收 Google 授權碼的回呼處理常式。這個範例說明設定用戶端、取得同意聲明,以及將授權碼傳送至後端平台所需的最少步驟。

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

GIS 重新導向使用者體驗

授權碼模式支援彈出式視窗和重新導向 UX 模式,可將每位使用者的授權碼傳送至平台代管的端點。重新導向使用者體驗模式如下所示:

<!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() 或 requestAccessToken() 以新換舊
GoogleUser.getBasicProfile() ,即可逐一移除離線觀看清單內的影片;請改用 ID 權杖,詳情請參閱「從 Google 登入服務遷移」。
GoogleUser.getGrantedScopes() hasGrantedAnyScope() 以新換舊
GoogleUser.getHostedDomain() 移除
GoogleUser.getId() 移除
GoogleUser.grantOfflineAccess() 移除,請按照授權碼流程操作。
GoogleUser.grant() 移除
GoogleUser.hasGrantedScopes() hasGrantedAnyScope() 以新換舊
GoogleUser.isSignedIn() 移除
GoogleUser.reloadAuthResponse() requestAccessToken() 移除舊權杖,呼叫新權杖,取代過期或撤銷的存取權杖。
gapi.auth2 物件和相關聯的方法:
gapi.auth2.AuthorizeConfig 個物件 TokenClientConfig 或 CodeClientConfig 以新換舊
gapi.auth2.AuthorizeResponse 個物件 移除
gapi.auth2.AuthResponse 個物件 移除
gapi.auth2.authorize() requestCode() 或 requestAccessToken() 以新換舊
gapi.auth2.ClientConfig() TokenClientConfig 或 CodeClientConfig 以新換舊
gapi.auth2.getAuthInstance() 移除
gapi.auth2.init() initTokenClient() 或 initCodeClient() 以新換舊
gapi.auth2.OfflineAccessOptions 個物件 移除
gapi.auth2.SignInOptions 個物件 移除
gapi.signin2 物件和相關方法:
gapi.signin2.render() ,即可逐一移除離線觀看清單內的影片;HTML DOM 載入 g_id_signin 元素或 JS 呼叫 google.accounts.id.renderButton,觸發使用者登入 Google 帳戶。

憑證範例

現有憑證

Google 登入平台程式庫、JavaScript 專用 Google API 用戶端程式庫,或直接呼叫 Google OAuth 2.0 端點,都會在單一回應中傳回 OAuth 2.0 存取權杖和 OpenID Connect ID 權杖。

同時包含 access_token 和 id_token 的回應範例:

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

Google Identity 服務憑證

Google Identity 服務程式庫會傳回:

  • 用於授權的存取權杖:

    {
      "access_token": "ya29.A0ARrdaM_LWSO-uckLj7IJVNSfnUityT0Xj-UCCrGxFQdxmLiWuAosnAKMVQ2Z0LLqeZdeJii3TgULp6hR_PJxnInBOl8UoUwWoqsrGQ7-swxgy97E8_hnzfhrOWyQBmH6zs0_sUCzwzhEr_FAVqf92sZZHphr0g",
      "token_type": "Bearer",
      "expires_in": 3599,
      "scope": "https://www.googleapis.com/auth/calendar.readonly"
    }
    
  • 或用於驗證的 ID 權杖:

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

權杖回應無效

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

HTTP 回應標頭

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

回應主體

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