遷移至 Google Identity 服務

總覽

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

本指南提供相關操作說明,協助您從這些程式庫遷移至 Google Identity 服務程式庫

按照本指南操作,即可完成下列事項:

  • 以 Identity 服務程式庫取代已淘汰的平台程式庫,以及
  • 如果使用 API 用戶端程式庫,請移除已淘汰的 gapi.auth2 模組、方法和物件,並改用 Identity Services 對等項目。

如要瞭解 Identity Services JavaScript 程式庫的變更內容,請參閱總覽使用者授權運作方式,查看重要術語和概念。

如要瞭解如何驗證使用者註冊和登入,請參閱「從 Google 登入服務遷移」一文。

找出授權流程

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

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

如果您的 Web 應用程式使用隱含流程,會有以下跡象:

網頁應用程式使用授權碼流程的跡象:

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

選擇授權流程

開始遷移前,請先判斷繼續使用現有流程,還是採用其他流程,最能滿足您的需求。

請參閱選擇授權流程,瞭解這兩種流程的主要差異和取捨。

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

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

隱含流程

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

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

授權碼流程

Google 核發的授權碼會傳送至後端平台,然後交換為存取權杖和更新權杖。

授權碼流程範例:顯示遷移至 Identity Services 前後的網路應用程式。

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

瀏覽器內網頁應用程式的變更

本節將說明遷移至 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 模組的使用方式。 您可以繼續安全地使用 Google API JavaScript 用戶端程式庫中的 gapi.client 模組,並利用其自動從探索文件建立可呼叫的 JS 方法、批次處理多個 API 呼叫,以及 CORS 管理功能。

Cookie

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

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

憑證

Google Identity Services 會將使用者驗證和授權分成兩種不同的作業,使用者憑證也會分開處理:用於識別使用者的 ID 權杖,會與用於授權的存取權杖分開傳回。

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

隱含流程

將使用者驗證和授權程序分開,從授權流程中移除使用者設定檔處理作業。

移除這些 Google 登入 JavaScript 用戶端參照

方法

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

授權碼流程

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

工作階段狀態

先前,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()

用戶端設定

更新 Web 應用程式,為隱含或授權碼流程初始化權杖用戶端。

移除這些 Google 登入 JavaScript 用戶端參照

物件:

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

方法:

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

隱含流程

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

Google 登入 JavaScript 用戶端參照替換為 Google Identity 服務

物件:

  • gapi.auth2.AuthorizeConfig (使用 TokenClientConfig 付款)

方法:

  • gapi.auth2.init() (使用 google.accounts.oauth2.initTokenClient() 付款)

參數:

  • gapi.auth2.AuthorizeConfig.login_hint (TokenClientConfig.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 Services

方法:

  • gapi.auth2.authorize() (使用 TokenClient.requestAccessToken() 付款)
  • GoogleUser.reloadAuthResponse() (TokenClient.requestAccessToken())

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

更新程式碼集,以符合下列條件:

  • 使用 requestAccessToken() 觸發 OAuth 2.0 權杖流程
  • 使用 requestAccessTokenOverridableTokenClientConfig 將多個範圍的一項要求,拆分成多項較小的要求,支援增量授權。
  • 現有權杖過期或遭撤銷時,請要求新權杖。

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

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

授權碼流程

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

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

權杖處理

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

如果使用過期或遭撤銷的存取權杖,Google API 會傳回 HTTP 狀態碼 401 Unauthorizedinvalid_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 權杖 credential email 欄位,做為 CodeClientConfigTokenClientConfig 物件中 login_hint 參數的值。如果您的平台沒有維護使用者帳戶管理系統,這個做法就非常實用。
  • 在平台上查詢並將 Google 帳戶與現有的本機使用者帳戶建立關聯,盡量減少平台上的重複帳戶。
  • 建立新的本機帳戶時,註冊對話方塊和流程可以與使用者驗證對話方塊和流程清楚區隔,減少必要步驟數量,並改善流失率。

登入後,使用者必須先同意應用程式要求範圍的權限,系統才會核發存取權杖。

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

使用者可以透過精細權限核准或拒絕個別範圍。要求存取多個範圍時,系統會分別核准或拒絕每個範圍,根據使用者的選擇,應用程式會選擇性啟用依個別範圍而定的功能。

隱含流程

Google 登入 JavaScript 用戶端參照替換為 Google Identity 服務

物件:

  • gapi.auth2.AuthorizeResponse (使用 TokenClient.TokenResponse 付款)
  • gapi.auth2.AuthResponse (使用 TokenClient.TokenResponse 付款)

方法:

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

移除 Google 登入 JavaScript 用戶端參照

方法:

  • GoogleUser.getAuthResponse()

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

授權碼流程

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

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

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

隱含流程範例

傳統做法

GAPI 用戶端程式庫

以下範例說明如何使用 Google API JavaScript 用戶端程式庫,在瀏覽器中透過彈出式對話方塊取得使用者同意聲明。

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:在瀏覽器中執行,並重新導向至 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 模組,以及使用 Google API JavaScript 專用用戶端程式庫呼叫 API。

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

如果網頁首次載入時缺少存取權杖,或存取權杖過期,使用者應按下「顯示日曆」按鈕。

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

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

        let tokenClient;
        let libsLoaded = 0;

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

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

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

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

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

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

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

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

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

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

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

GAPI 回呼

本範例說明如何使用權杖模型新增 Google Identity 服務程式庫、移除 gapi.auth2 模組,以及使用 Google API JavaScript 專用用戶端程式庫呼叫 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 帳戶登入服務程式庫彈出式 UX 可以使用網址重新導向,直接將授權碼傳回後端權杖端點,也可以使用在使用者瀏覽器中執行的 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 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 重新導向使用者體驗

授權碼模型支援彈出式視窗和重新導向 UX 模式,可將每個使用者的授權碼傳送至平台代管的端點。重新導向 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() or requestAccessToken() 以新值取代舊值
GoogleUser.getBasicProfile() ,即可逐一移除離線觀看清單內的影片;請改用 ID 權杖,詳情請參閱「從 Google 登入遷移」。
GoogleUser.getGrantedScopes() hasGrantedAnyScope() 以新值取代舊值
GoogleUser.getHostedDomain() 移除
GoogleUser.getId() 移除
GoogleUser.grantOfflineAccess() 移除,然後按照授權碼流程操作。
GoogleUser.grant() 移除
GoogleUser.hasGrantedScopes() hasGrantedAnyScope() 以新值取代舊值
GoogleUser.isSignedIn() 移除
GoogleUser.reloadAuthResponse() requestAccessToken() 移除舊權杖,並呼叫新權杖來取代過期或遭撤銷的存取權杖。
gapi.auth2 物件和相關方法:
gapi.auth2.AuthorizeConfig 物件 TokenClientConfig 或 CodeClientConfig 以新值取代舊值
gapi.auth2.AuthorizeResponse 物件 移除
gapi.auth2.AuthResponse 物件 移除
gapi.auth2.authorize() requestCode() or requestAccessToken() 以新值取代舊值
gapi.auth2.ClientConfig() TokenClientConfig 或 CodeClientConfig 以新值取代舊值
gapi.auth2.getAuthInstance() 移除
gapi.auth2.init() initTokenClient() or initCodeClient() 以新值取代舊值
gapi.auth2.OfflineAccessOptions 物件 移除
gapi.auth2.SignInOptions 物件 移除
gapi.signin2 物件和相關方法:
gapi.signin2.render() ,即可逐一移除離線觀看清單內的影片;HTML DOM 載入 g_id_signin 元素,或 JS 呼叫 google.accounts.id.renderButton,都會觸發使用者登入 Google 帳戶。

憑證範例

現有憑證

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

Google Identity Services 程式庫會傳回:

  • 用於授權的存取權杖:

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

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

權杖回應無效

嘗試使用過期、遭撤銷或無效的存取權權杖發出 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"
    }
  }