在 App Engine 中登入的使用者

本教學課程說明如何使用 Identity Platform、App Engine 標準環境和 Datastore,擷取、驗證及儲存第三方憑證。

本文件將逐步說明如何使用 Firenotes 這個簡易的筆記工具應用程式,將使用者的筆記儲存在自己的個人筆記本中。筆記本會依個別使用者進行儲存,並透過每位使用者的專屬 Identity Platform ID 加以識別。應用程式包含下列元件:

  • 前端設定登入使用者介面及擷取 Identity Platform ID,也可處理驗證狀態變更並讓使用者查看其筆記。

  • FirebaseUI 是一套開放原始碼的置入式解決方案,可簡化驗證和 UI 工作。這個 SDK 可處理使用者登入、將多個供應商連結到一個帳戶、還原密碼及其他作業。它採用了驗證的最佳做法,提供流暢安全的登入體驗。

  • 後端會驗證使用者的驗證狀態,並傳回使用者個人資料和其筆記。

這個應用程式使用 NDB 用戶端程式庫將使用者憑證儲存在 Datastore 中,但您可以將憑證儲存在自選資料庫中。

Firenotes 以 Flask 網路應用程式架構為基礎。範例應用程式使用 Flask 是因為它簡單易用。但不論您使用哪一個架構,本文探討的概念和技術皆適用。

複製範例應用程式

如要將範例應用程式下載到您的本機電腦上:

  1. 將範例應用程式存放區複製到本機電腦:

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git

    您也可以下載 zip 格式的範例檔案,之後再將檔案解壓縮。

  2. 前往包含程式碼範例的目錄:

    cd python-docs-samples/appengine/standard/firebase/firenotes
    

新增使用者介面

如何設定 Identity Platform 的 FirebaseUI 並啟用身分識別提供者:

  1. 按照下列步驟將 Identity Platform 新增至應用程式:

    1. 前往Google Cloud 控制台
      前往 Google Cloud 控制台
    2. 選取要使用的 Google Cloud 專案:
      • 如果您有現成專案,請在頁面頂端的「Select organization」(選取機構) 下拉式清單中選取該專案。
      • 如果沒有現有 Google Cloud 專案,請在Google Cloud 控制台中建立新專案
    3. 前往 Google Cloud 控制台的「Identity Platform Marketplace」頁面。
      前往 Identity Platform Marketplace 頁面
    4. 在 Identity Platform Marketplace 頁面中,按一下「啟用 Customer Identity」
    5. 前往 Google Cloud 控制台的「Customer Identity」使用者頁面。
      前往「Users」(使用者) 頁面
    6. 按一下右上方的「應用程式設定詳細資料」
    7. 將應用程式設定詳細資料複製到網頁應用程式。

      // This code is for illustration purposes only.
      
      // Obtain the following from the "Add Firebase to your web app" dialogue
      // Initialize Firebase
      var config = {
        apiKey: "<API_KEY>",
        authDomain: "<PROJECT_ID>.firebaseapp.com",
        databaseURL: "https://<DATABASE_NAME>.firebaseio.com",
        projectId: "<PROJECT_ID>",
        storageBucket: "<BUCKET>.appspot.com",
        messagingSenderId: "<MESSAGING_SENDER_ID>"
      };
  2. 編輯 backend/app.yaml 檔案,在 env_variables 區段中新增 GOOGLE_CLOUD_PROJECT : 'PROJECT_ID'

    # Copyright 2021 Google LLC
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #      http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    
    # This code is designed for Python 2.7 and
    # the App Engine first-generation Runtime which has reached End of Support.
    
    runtime: python27
    api_version: 1
    threadsafe: true
    service: backend
    
    handlers:
    - url: /.*
      script: main.app
    
    env_variables:
      GAE_USE_SOCKETS_HTTPLIB : 'true'
    
  3. frontend/main.js 檔案中選取您要提供給使用者的供應商,以設定 FirebaseUI 登入小工具

    // This code is for illustration purposes only.
    
    // Firebase log-in widget
    function configureFirebaseLoginWidget() {
      var uiConfig = {
        'signInSuccessUrl': '/',
        'signInOptions': [
          // Leave the lines as is for the providers you want to offer your users.
          firebase.auth.GoogleAuthProvider.PROVIDER_ID,
          firebase.auth.FacebookAuthProvider.PROVIDER_ID,
          firebase.auth.TwitterAuthProvider.PROVIDER_ID,
          firebase.auth.GithubAuthProvider.PROVIDER_ID,
          firebase.auth.EmailAuthProvider.PROVIDER_ID
        ],
        // Terms of service url
        'tosUrl': '<your-tos-url>',
      };
    
      var ui = new firebaseui.auth.AuthUI(firebase.auth());
      ui.start('#firebaseui-auth-container', uiConfig);
    }
  4. 在 Google Cloud 控制台啟用您選擇保留的供應商:

    1. 前往 Google Cloud 控制台的「Customer Identity Providers」(客戶身分識別提供者) 頁面。
      前往「Providers」(供應商) 頁面
    2. 按一下「Add A Provider」
    3. 在「選取供應商」下拉式清單中,選取要使用的供應商。
    4. 按一下「已啟用」旁的按鈕,啟用供應商。
      • 如果是第三方識別資訊提供者,請輸入取自供應商開發人員網站的供應商 ID 和密碼。Firebase 文件會提供 FacebookTwitterGitHub 指南「事前準備」小節中的特定操作說明。
      • 如要整合 SAML 和 OIDC,請參閱 IdP 的設定。
  5. 在 Identity Platform 中將網域新增至授權網域清單:

    1. 前往 Google Cloud 控制台的「Customer Identity Settings」(客戶身分設定) 頁面。
      前往「設定」頁面
    2. 在「已授權網域」下方,按一下「新增網域」
    3. 請按照下列格式輸入應用程式的網域:

      [PROJECT_ID].appspot.com
      

      請勿在網域名稱前加上 http://

安裝依附元件

  1. 前往 backend 目錄,然後完成應用程式設定:

    cd backend/
    
  2. 將依附元件安裝到專案的 lib 目錄中:

    pip install -t lib -r requirements.txt
    
  3. appengine_config.py 中,vendor.add() 方法會註冊 lib 目錄中的程式庫。

在本機執行應用程式

如要在本機執行應用程式,請使用 App Engine 本機開發伺服器:

  1. main.js 中將下列網址新增為 backendHostURL

    http://localhost:8081

  2. 前往應用程式的根目錄,然後啟動開發伺服器:

    dev_appserver.py frontend/app.yaml backend/app.yaml
    
  3. 使用網路瀏覽器前往 http://localhost:8080/

在伺服器上驗證使用者

您已設定專案並初始化應用程式以進行開發,現在可以逐步瞭解程式碼,瞭解如何在伺服器上擷取及驗證 Identity Platform ID 權杖。

從 Identity Platform 取得 ID 權杖

伺服器端驗證的第一步是擷取要驗證的存取憑證。驗證要求會透過 Identity Platform 的 onAuthStateChanged() 監聽器處理:

firebase.auth().onAuthStateChanged(function (user) {
  if (user) {
    $('#logged-out').hide();
    var name = user.displayName;

    /* If the provider gives a display name, use the name for the
    personal welcome message. Otherwise, use the user's email. */
    var welcomeName = name ? name : user.email;

    user.getIdToken().then(function (idToken) {
      userIdToken = idToken;

      /* Now that the user is authenicated, fetch the notes. */
      fetchNotes();

      $('#user').text(welcomeName);
      $('#logged-in').show();

    });

  } else {
    $('#logged-in').hide();
    $('#logged-out').show();

  }
});

使用者登入後,回呼中的 Identity Platform getToken() 方法會傳回 Identity Platform ID 憑證,格式為 JSON Web Token (JWT)。

在伺服器上驗證憑證

使用者登入後,前端服務會透過 AJAX GET 要求,擷取使用者記事本中的所有現有記事。這項操作需要授權才能存取使用者資料,因此系統會使用 Bearer 結構定義,在要求的 Authorization 標頭中傳送 JWT:

// Fetch notes from the backend.
function fetchNotes() {
  $.ajax(backendHostUrl + '/notes', {
    /* Set header for the XMLHttpRequest to get data from the web server
    associated with userIdToken */
    headers: {
      'Authorization': 'Bearer ' + userIdToken
    }
  })

在用戶端可以存取伺服器資料之前,伺服器必須先驗證該憑證是否由 Identity Platform 簽署。您可以使用 Python 專用 Google 驗證程式庫來驗證這個權杖。 使用驗證資料庫的 verify_firebase_token 函式來驗證不記名憑證及擷取相關宣告:

# This code is for illustration purposes only.

id_token = request.headers["Authorization"].split(" ").pop()
claims = google.oauth2.id_token.verify_firebase_token(
    id_token, HTTP_REQUEST, audience=os.environ.get("GOOGLE_CLOUD_PROJECT")
)
if not claims:
    return "Unauthorized", 401

每個身分識別提供者都會傳送不同的一組聲明,但每組聲明都至少有含有唯一使用者 ID 的 sub 聲明,以及用於提供某些個人資料資訊的聲明,如 nameemail,可供您用來將應用程式的使用者體驗,進行個人化處理。

管理 Datastore 中的使用者資料

驗證使用者後,您必須儲存他們的資料,才能在登入工作階段結束後保留資料。以下各節說明如何將記事儲存為 Datastore 實體,並依使用者 ID 分隔實體。

建立實體以儲存使用者資料

您可以宣告含有整數或字串等特定屬性的 NDB 模型類別,在 Datastore 中建立實體。Datastore 會依「種類」建立實體索引;以 Firenotes 為例,每個實體的種類都是 Note。 為方便查詢,每個 Note 會和「鍵名稱」一起儲存,這是從上一節的 sub 宣告中取得的使用者 ID。

以下程式碼會示範如何設定實體的屬性,包括在建立實體時,如何使用模型類別的建構函式方法進行設定,以及建立實體後,如何指派個別屬性的方式:

# This code is for illustration purposes only.

data = request.get_json()

# Populates note properties according to the model,
# with the user ID as the key name.
note = Note(parent=ndb.Key(Note, claims["sub"]), message=data["message"])

# Some providers do not provide one of these so either can be used.
note.friendly_id = claims.get("name", claims.get("email", "Unknown"))

如要將新建立的 Note 寫入 Datastore,請針對 note 物件呼叫 put() 方法。

擷取使用者資料

如要擷取與特定使用者 ID 相關的使用者資料,請使用 NDB query() 方法搜尋資料庫,即可取得相同實體群組中的筆記。相同群組中的實體,或祖系路徑,會共用一個鍵名稱,在此範例中是指使用者 ID。

# This code is for illustration purposes only.

def query_database(user_id):
    """Fetches all notes associated with user_id.

    Notes are ordered them by date created, with most recent note added
    first.
    """
    ancestor_key = ndb.Key(Note, user_id)
    query = Note.query(ancestor=ancestor_key).order(-Note.created)
    notes = query.fetch()

    note_messages = []

    for note in notes:
        note_messages.append(
            {
                "friendly_id": note.friendly_id,
                "message": note.message,
                "created": note.created,
            }
        )

    return note_messages

您可以接著擷取查詢資料,並在用戶端顯示筆記:

// This code is for illustration purposes only.

// Fetch notes from the backend.
function fetchNotes() {
  $.ajax(backendHostUrl + '/notes', {
    /* Set header for the XMLHttpRequest to get data from the web server
    associated with userIdToken */
    headers: {
      'Authorization': 'Bearer ' + userIdToken
    }
  }).then(function (data) {
    $('#notes-container').empty();
    // Iterate over user data to display user's notes from database.
    data.forEach(function (note) {
      $('#notes-container').append($('<p>').text(note.message));
    });
  });
}

部署您的應用程式

您已成功整合 Identity Platform 與 App Engine 應用程式。如要查看在即時實際工作環境中執行的應用程式:

  1. main.js 中的後端主機網址變更為 https://backend-dot-[PROJECT_ID].appspot.com。將 [PROJECT_ID] 替換為您的專案 ID。
  2. 使用 Google Cloud SDK 指令列介面部署應用程式:

    gcloud app deploy backend/index.yaml frontend/app.yaml backend/app.yaml
    
  3. 前往 https://[PROJECT_ID].appspot.com 查看上線的應用程式。