Python 2 不再受社区支持。我们建议您将 Python 2 应用迁移到 Python 3

使用 Firebase 在 App Engine 上对用户进行身份验证

本教程介绍如何使用 Firebase 身份验证、App Engine 标准环境和 Datastore 来检索、验证和存储用户凭据。

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

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

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

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

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

下图显示了前端与后端如何相互通信以及用户凭据如何从 Firebase 传输到数据库。
使用用户凭据的请求的路径

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

目标

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

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

费用

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

  • 数据存储区

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

准备工作

  1. 安装 GitPython 2.7virtualenv。如需了解有关设置 Python 开发环境的详情(例如安装最新版本的 Python),请参阅 Google Cloud 的设置 Python 开发环境
  2. 登录您的 Google 帐号。

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

  3. 在 Cloud Console 的项目选择器页面上,选择或创建 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 并启用身份提供商,请执行以下操作:

  3. 按照以下步骤将 Firebase 添加到您的应用:

    1. Firebase 控制台中创建一个 Firebase 项目。
      • 如果您当前没有 Firebase 项目,请点击添加项目,然后输入现有 Google Cloud 项目名称或新项目名称。
      • 如果您有一个打算使用的现有 Firebase 项目,请从控制台中选择该项目。
    2. 在项目概览页面中,点击将 Firebase 添加到您的网页应用。如果您的项目已有应用,请从项目概览页面中选择添加应用
    3. 使用项目自定义代码段的 Initialize Firebase 部分填写 frontend/main.js 文件的以下部分:

      // 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>"
      };
  4. 修改 backend/app.yaml 文件并在环境变量中输入您的 Firebase 项目 ID:

    runtime: python27
    api_version: 1
    threadsafe: true
    service: backend
    
    handlers:
    - url: /.*
      script: main.app
    
    env_variables:
      GAE_USE_SOCKETS_HTTPLIB : 'true'
    
  5. 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();
    
    });
    
  6. 依次点击 Authentication(身份验证)> Sign-in method(登录方法),启用您选择要保留在 Firebase 控制台中的提供商。然后,在登录提供商下,将光标悬停在提供商上,然后点击铅笔图标。

    登录提供商

    1. 切换启用按钮,对于第三方身份提供商,请输入提供商开发者网站上的提供商 ID 和密钥。Firebase 文档会提供 FacebookTwitterGitHub 指南“准备工作”部分中的具体说明。启用提供商后,点击保存

      切换启用按钮

    2. 在 Firebase 控制台中,点击已获授权的网域下的添加网域,然后按以下格式在 App Engine 上输入应用的网域:

      [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/

在服务器上对用户进行身份验证

现在您已设置了一个项目并初始化了一个用于开发的应用,接下来您可以浏览代码,以了解如何在服务器上检索和验证 Firebase ID 令牌。

从 Firebase 获取 ID 令牌

服务器端身份验证的第一步是检索要验证的访问令牌。身份验证请求通过 Firebase 中的 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();

  }
});

用户登录后,回调中的 Firebase getToken() 方法将以 JSON 网络令牌 (JWT) 形式返回 Firebase 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
    }
  })

在客户端可以访问服务器数据之前,您的服务器必须验证令牌是否已由 Firebase 签名。您可以使用 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

每个身份提供商都会发送一组不同的声明,但每个提供者至少有一个 sub 声明(具有唯一的用户 ID)和一个提供一些资料信息的声明(如 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));
    });
  });
}

部署应用

您已成功地将 Firebase 身份验证与 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,然后点击关闭以删除项目。

后续步骤