在 App Engine 上登录用户

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

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

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

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

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

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

Firenotes 基于 Flask Web 应用框架。该示例应用之所以使用 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 帐号。

    如果您还没有 Google 帐号,请注册新帐号

  3. 在 Google Cloud Console 的项目选择器页面上,选择或创建一个 Google Cloud 项目。

    转到项目选择器页面

  4. 安装并初始化 Cloud SDK

如果您已为其他项目安装并初始化 SDK,请将 gcloud 项目设置为您用于 Firenotes 的 App Engine 项目 ID。如需了解使用 gcloud 工具更新项目需要用到的特定命令,请参阅管理 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. 转到 Cloud Console
      转到 Cloud Console
    2. 选择您要使用的 Google Cloud 项目:
      • 如果您已有项目,请在页面顶部的选择组织下拉列表中选择该项目。
      • 如果您还没有 Google Cloud 项目,请在 Cloud Console 中创建新项目
    3. 转到 Cloud Console 中的 Identity Platform Marketplace 页面。
      转到 Identity Platform Marketplace 页面
    4. 在 Identity Platform Marketplace 页面上,点击启用客户身份 (Enable Customer Identity)。
    5. 转到 Cloud Console 中的客户身份用户页面。
      转到“用户”页面
    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:

    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);
      }
    
      // 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));
          });
        });
      }
    
      // Sign out a user
      var signOutBtn =$('#sign-out');
      signOutBtn.click(function(event) {
        event.preventDefault();
    
        firebase.auth().signOut().then(function() {
          console.log("Sign out successful");
        }, function(error) {
          console.log(error);
        });
      });
    
      // Save a note to the backend
      var saveNoteBtn = $('#add-note');
      saveNoteBtn.click(function(event) {
        event.preventDefault();
    
        var noteField = $('#note-content');
        var note = noteField.val();
        noteField.val("");
    
        /* Send note data to backend, storing in database with existing data
        associated with userIdToken */
        $.ajax(backendHostUrl + '/notes', {
          headers: {
            'Authorization': 'Bearer ' + userIdToken
          },
          method: 'POST',
          data: JSON.stringify({'message': note}),
          contentType : 'application/json'
        }).then(function(){
          // Refresh notebook display.
          fetchNotes();
        });
    
      });
    
      configureFirebaseLogin();
      configureFirebaseLoginWidget();
    
    });
    
  4. 在 Cloud Console 中,启用您选择要保留的提供商:

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

    1. 转到 Cloud Console 中的客户身份设置页面。
      转到“设置”页面
    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 架构将 JWT 发送到该请求的 Authorization 标头中:

// 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. 使用 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. 在 Cloud Console 中,转到管理资源页面。

    转到“管理资源”

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

后续步骤