在 App Engine 上登录用户


本教程介绍如何使用 Identity Platform、App Engine 标准环境和 Datastore 检索、验证和存储第三方凭据。

本文档将介绍一款名为 Firenotes 的简单笔记应用,该应用可将用户笔记存储在用户自己的个人笔记本中。笔记本按用户存储,并由每个用户唯一的 Identity Platform ID 进行标识。该应用包含以下组件:

  • 前端 - 前端配置登录界面并检索 Identity Platform ID。它还会处理身份验证状态更改,并允许用户查看其记事。

  • FirebaseUI - 是一个具有普适性的开源解决方案,可简化身份验证和界面任务。SDK 会处理用户登录、将多个提供商关联到一个账号、恢复密码等。FirebaseUI 会实施身份验证最佳做法,可带来流畅、安全的登录体验。

  • 后端 - 后端负责验证用户的身份验证状态,并返回用户个人资料信息以及用户的笔记。

该应用使用 NDB 客户端库在 Datastore 中存储用户凭据,但您可以将凭据存储在您选择的数据库中。

Firenotes 基于 Flask 网络应用框架。该示例应用之所以使用 Flask,是因为该框架简单易用,但无论您使用哪种框架,此处所探讨的概念和技术都适用。

目标

学完本教程后,您将可以完成以下任务:

  • 使用 FirebaseUI for Identity Platform 配置界面。
  • 获取 Identity Platform ID 令牌并使用服务器端身份验证对其进行验证。
  • 将用户凭据和关联的数据存储到 Datastore 中。
  • 使用 NDB 客户端库查询数据库。
  • 将应用部署到 App Engine。

费用

本教程使用 Google Cloud 的以下收费组件:

  • Datastore
  • Identity Platform

您可使用价格计算器根据您的预计使用情况来估算费用。Google Cloud 新用户可能有资格申请免费试用

准备工作

  1. 安装 GitPython 2.7virtualenv。如需了解有关设置 Python 开发环境的详情(例如安装最新版本的 Python),请参阅 Google Cloud 的设置 Python 开发环境
  2. 登录您的 Google Cloud 账号。如果您是 Google Cloud 新手,请创建一个账号来评估我们的产品在实际场景中的表现。新客户还可获享 $300 赠金,用于运行、测试和部署工作负载。
  3. 在 Google Cloud Console 中的项目选择器页面上,选择或创建一个 Google Cloud 项目

    转到“项目选择器”

  4. 安装 Google Cloud CLI。
  5. 如需初始化 gcloud CLI,请运行以下命令:

    gcloud init
  6. 在 Google Cloud Console 中的项目选择器页面上,选择或创建一个 Google Cloud 项目

    转到“项目选择器”

  7. 安装 Google Cloud CLI。
  8. 如需初始化 gcloud CLI,请运行以下命令:

    gcloud init

如果您已为其他项目安装并初始化 SDK,请将 gcloud 项目设置为您用于 Firenotes 的 App Engine 项目 ID。如需了解使用 gcloud 工具更新项目需要用到的特定命令,请参阅管理 Google Cloud SDK 配置

克隆示例应用

要将示例下载到本地计算机,请执行以下操作:

  1. 将示例应用代码库克隆到本地计算机:

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

    或者,您也可以下载该示例的 zip 文件并将其解压缩。

  2. 导航到包含示例代码的目录:

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

添加界面

如需配置 FirebaseUI for Identity Platform 并启用身份提供商,请执行以下操作:

  1. 按照以下步骤将 Identity Platform 添加到您的应用:

    1. 前往 Google Cloud 控制台
      转到 Google Cloud 控制台
    2. 选择您要使用的 Google Cloud 项目:
      • 如果您已有项目,请在页面顶部的选择组织下拉列表中选择该项目。
      • 如果您还没有 Google Cloud 项目,请在 Google Cloud 控制台中创建一个新项目
    3. 转到 Google Cloud 控制台中的 Identity Platform Marketplace 页面。
      转到 Identity Platform Marketplace 页面
    4. 在 Identity Platform Marketplace 页面上,点击启用客户身份 (Enable Customer Identity)。
    5. 前往 Google Cloud 控制台中的客户身份用户页面。
      转到“用户”页面
    6. 点击右上角的应用设置详情
    7. 将应用设置详情复制到您的 Web 应用中。

      // 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 文件,并在环境变量中输入您的 Google Cloud 项目 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.
    
    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 登录微件

    // 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 控制台中的客户身份提供商页面。
      转到“提供商”页面
    2. 点击添加提供商
    3. 选择一个提供商下拉列表中,选择要使用的提供商。
    4. 点击已启用旁边的按钮以启用提供商。
      • 对于第三方身份提供商,请输入提供商开发者网站上的提供商 ID 和密钥。Firebase 文档会提供 FacebookTwitterGitHub 指南“准备工作”部分中的具体说明。
      • 对于 SAML 和 OIDC 集成,请参阅 IdP 的配置。
  5. 将您的网域添加到 Identity Platform 中的已获授权的网域列表中:

    1. 前往 Google Cloud 控制台中的客户身份设置页面。
      转到“设置”页面
    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() 方法将以 JSON Web 令牌 (JWT) 形式返回 Identity Platform ID 令牌。

在服务器上验证令牌

用户登录后,前端服务会通过 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 函数验证不记名令牌并提取声明:

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。

以下代码演示了如何设置实体的属性(在创建实体时,通过模型类的构造函数方法进行设置;在创建实体后,通过分配各个属性进行设置):

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。

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

然后,您可以提取查询数据并在客户端显示笔记:

// 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 上实时查看应用。

清理

为避免系统因本教程中使用的资源而向您的 Google Cloud 账号收取费用,请删除 App Engine 项目:

删除项目

若要避免产生费用,最简单的方法是删除您为本教程创建的项目。

如需删除项目,请执行以下操作:

  1. 在 Google Cloud 控制台中,进入管理资源页面。

    转到“管理资源”

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关闭以删除项目。

后续步骤