遷移至 Google Identity 服務

總覽

為了取得每個使用者存取 Google API 的存取權杖,Google 提供 JavaScript 程式庫:

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

按照這份指南操作,您就能:

  • 將已淘汰的平台程式庫替換為 Identity Services 程式庫。 和
  • 如果您使用 API 用戶端程式庫,請移除已淘汰的 gapi.auth2 模組。 方法和物件,替換為對應的 Identity Services。

瞭解 Identity Services JavaScript 的變更項目說明 請詳閱總覽使用者授權的運作方式, 重要詞彙及概念

如需使用者註冊和登入驗證的資訊,請參閱 請改為從 Google 登入遷移

確定授權流程

可能的使用者授權流程有兩種:隱含和授權 再也不是件繁重乏味的工作

查看網頁應用程式,找出目前的授權流程類型

表示網頁應用程式使用隱含流程

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

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

選擇授權流程

在開始遷移之前,您必須決定是要繼續進行 最好能滿足您的需求,選擇合適的流程或採用不同的流程

請參閱選擇授權流程一文,瞭解主要差異 這兩種流程間的權衡取捨

在大部分的情況下,我們會建議您使用授權碼流程,因為此流程提供了 最高等級的使用者安全導入這個流程後 平台,更輕鬆地新增離線功能,例如擷取更新 通知使用者日曆、相片、訂閱項目及相關異動有重大異動 依此類推

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

隱含流程

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

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

授權碼流程

系統會將由 Google 核發的使用者授權碼傳送至您的後端 平台中,用於交換存取權杖與更新權杖。

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

在本指南中,請遵照粗體顯示的「新增」說明, 移除更新取代現有功能。

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

本節會審查您在瀏覽器網頁應用程式上,進行的變更。 遷移至 Google Identity Services JavaScript 程式庫。

找出受影響的程式碼並進行測試

偵錯 Cookie 可協助找出受影響的程式碼,並在淘汰後測試 行為

在大型或複雜的應用程式中,可能不容易找出所有受 淘汰 gapi.auth2 模組。如要記錄近期的使用情況 請前往控制台,將「 G_AUTH2_MIGRATION 個 Cookie 傳送給 informational。視需要新增半形冒號 的鍵值,一併記錄到工作階段儲存空間。登入後 確認是否已順利檢查憑證,或是將收集到的記錄傳送至後端 以便查詢及分析舉例來說,informational:showauth2use 會將來源和網址儲存到 名為 showauth2use 的工作階段儲存空間金鑰

如要在 gapi.auth2 模組不再載入時驗證應用程式行為,請設定 G_AUTH2_MIGRATION Cookie 的值設為 enforced。如此一來 在違規處置日期前,採取淘汰後的行為。

可能的 G_AUTH2_MIGRATION 個 Cookie 值:

  • enforced不要載入 gapi.auth2 模組。
  • informational將已淘汰功能的使用情況記錄到 JS 控制台。一併記錄 如果設定選用的金鑰名稱,就會: informational:key-name

為盡量減少使用者受到的影響,建議先在本機設定這個 Cookie 仍在開發和測試階段,然後再用於正式環境。

程式庫與模組

gapi.auth2 模組可用於管理登入驗證和隱性 授權流程,取代這個淘汰的模組及其物件 方法。

在網頁應用程式中加入 Identity Services 程式庫,即可新增至網頁應用程式 文件:

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

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

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

Cookie

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

如要進一步瞭解使用者驗證方式,請參閱從 Google 登入遷移一文 採用 Cookie,以及 Google 如何將 Cookie 用於其他目的使用 Cookie Google 產品和服務。

憑證

Google Identity 服務將使用者驗證和授權作業分開 這兩種不同的操作,以及使用者憑證是各自獨立的: 識別使用者的身分會與用於存取權杖的存取權杖分開 或授權。

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

隱含流程

移除使用者設定檔,分別進行使用者驗證和授權 授權流程來處理。

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

方法

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

授權碼流程

Identity Services 將瀏覽器內的憑證區分為 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() 呼叫來進行設定 這樣才能按照初始化 Code Client 的範例來編寫。

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

移除 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 不支援自動權杖更新程序 服務程式庫。必須更新網頁應用程式,才能偵測存取權已過期 並索取新的權杖詳情請參閱下方的「權杖處理」一節。

授權碼流程

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

想進一步瞭解如何回應 過期或撤銷的存取權杖

權杖處理

Add 錯誤處理機制來偵測過期的 Google API 呼叫, 並使用已撤銷的存取權杖請求新的有效存取權杖。

401 Unauthorized」和「invalid_token」的 HTTP 狀態碼為 使用過期或撤銷的存取權杖時,Google API 會傳回 值。對於 範例:請參閱「權杖回應無效」。

過期的權杖

存取權杖的效期很短,而且通常只會持續幾分鐘。

撤銷權杖

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 帳戶登入程序 不存在。
  • 直接使用 JWT ID 權杖 credential email 欄位做為 CodeClientConfigTokenClientConfig 中的 login_hint 參數 如需儲存大量結構化物件 建議使用 Cloud Bigtable如果您的平台沒有維護 使用者帳戶管理系統
  • 在 查詢 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 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。

這個範例顯示從 不採用 gapi.auth2 模組或 JavaScript 資源庫。

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

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

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

  /*

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

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

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

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

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

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

新做法

僅限 GIS

這個範例僅顯示 Google Identity Service JavaScript 程式庫 使用權杖模型和彈出式對話方塊,徵詢使用者同意。是 說明設定 用戶端、要求取得存取權杖,以及呼叫 Google API。

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

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

GAPI 非同步/等待中

本範例說明如何使用 權杖模型,移除 gapi.auth2 模組,並使用 適用於 JavaScript 的 Google API 用戶端程式庫

Promis、async 和 await 用於強制執行程式庫的載入順序,以及 擷取並重試授權錯誤。API 呼叫只有在有效的 存取權杖

使用者應按下 [顯示日曆]存取權杖後 載入時遺失 已過期。

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

  <script>

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

    var tokenClient;

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

      // First, load and initialize the gapi.client
      await gapiLoadPromise;
      await new Promise((resolve, reject) => {
        // NOTE: the 'auth2' module is no longer loaded.
        gapi.load('client', {callback: resolve, onerror: reject});
      });
      await gapi.client.init({
        // NOTE: OAuth2 'scope' and 'client_id' parameters have moved to initTokenClient().
      })
      .then(function() {  // Load the Calendar API discovery document.
        gapi.client.load('https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest');
      });

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

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

    async function getToken(err) {

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

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

    function showEvents() {

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

    function revokeToken() {
      let cred = gapi.client.getToken();
      if (cred !== null) {
        google.accounts.oauth2.revoke(cred.access_token, () => {console.log('Revoked: ' + cred.access_token)});
        gapi.client.setToken('');
      }
    }

  </script>

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

</body>
</html>

GAPI 回呼

本範例說明如何使用 權杖模型,移除 gapi.auth2 模組,並使用 適用於 JavaScript 的 Google API 用戶端程式庫

變數的用途是強制執行程式庫的載入順序。我們會發出 GAPI 呼叫 。

使用者應在網頁首次開啟時按下「顯示日曆」按鈕 以重新整理自己的日曆資訊。

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

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

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

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

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

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

    function showEvents() {

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

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

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

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

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

授權碼流程範例

Google Identity 服務程式庫的彈出式視窗 UX 可以使用網址重新導向, 將授權碼直接傳回後端權杖端點 在使用者瀏覽器中執行的 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 Service JavaScript 程式庫 使用授權碼模型時,系統會顯示使用者同意聲明的彈出式對話方塊 以便接收 Google 的授權碼。是 說明設定 用戶端,取得同意聲明並將授權碼傳送至後端 平台。

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

GIS 重新導向使用者體驗

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

<!DOCTYPE html>
<html>
  <head>
    <script src="https://accounts.google.com/gsi/client" onload="initClient()" async defer></script>
  </head>
  <body>
    <script>
      var client;
      function initClient() {
        client = google.accounts.oauth2.initCodeClient({
          client_id: 'YOUR_CLIENT_ID',
          scope: 'https://www.googleapis.com/auth/calendar.readonly \
                  https://www.googleapis.com/auth/photoslibrary.readonly',
          ux_mode: 'redirect',
          redirect_uri: 'YOUR_AUTHORIZATION_CODE_ENDPOINT_URI'
        });
      }
      // Request an access token
      function getAuthCode() {
        // Request authorization code and obtain user consent
        client.requestCode();
      }
    </script>
    <button onclick="getAuthCode();">Load Your Calendar</button>
  </body>
</html>

JavaScript 程式庫

Google Identity 服務是供使用者使用的單一 JavaScript 程式庫 的驗證及授權,能夠整合與取代功能, 功能:

遷移至 Identity 服務時的建議做法:

現有的 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 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"
    }
    

權杖回應無效

嘗試使用 存取權杖已過期、撤銷或無效:

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