从 App Engine 用户服务迁移到 Cloud Identity Platform(模块 21)

1. 概览

无服务器迁移站系列 Codelab(自定进度的动手教程)和相关视频旨在帮助 Google Cloud 无服务器开发者通过一次或多次迁移(主要是从旧版服务迁移)来指导他们的应用现代化。这样做可让您的应用更易于移植,并为您提供更多选择和灵活性,使您能够与更多 Cloud 产品集成并访问这些产品,还能更轻松地升级到新的语言版本。虽然最初侧重于最早的 Cloud 用户(主要是 App Engine [标准环境] 开发者),但本系列文章的范围足够广,可涵盖其他无服务器平台(例如 Cloud FunctionsCloud Run),或者其他平台(如果适用)。

此 Codelab 的目的是向 Python 2 App Engine 开发者展示如何从 App Engine Users API/服务迁移到 Cloud Identity Platform (GCIP)。此外,对于 Datastore 访问权限(主要在迁移模块 2 中介绍),还有从 App Engine NDB 到 Cloud NDB隐式迁移,以及升级到 Python 3。

模块 20 介绍了如何将 Users API 的使用添加到模块 1 示例应用。在本模块中,您将使用已完成的模块 20 应用,并将其使用迁移到 Cloud Identity Platform。

在接下来的实验中

  • App Engine Users 服务的使用替换为 Cloud Identity Platform
  • App Engine NDB 的使用替换为 Cloud NDB(另请参阅模块 2)
  • 使用 Firebase Auth 设置不同的身份验证身份提供方
  • 使用 Cloud Resource Manager API 获取项目 IAM 信息
  • 使用 Firebase Admin SDK 获取用户信息
  • 将示例应用移植到 Python 3

所需条件

调查问卷

您打算如何使用本教程?

仅通读 阅读并完成练习

您如何评价使用 Python 的体验?

新手水平 中等水平 熟练水平

您如何评价自己在使用 Google Cloud 服务方面的经验水平?

新手水平 中等水平 熟练水平

2. 背景

App Engine Users 服务是一个用户身份验证系统,供 App Engine 应用使用。它提供 Google 登录作为身份提供方,提供可在应用中使用的便捷登录和退出链接,并支持管理员用户和仅限管理员使用的功能。为了提高应用的可移植性,Google Cloud 建议从旧版 App Engine 捆绑服务迁移到 Cloud 独立服务,例如从 Users 服务迁移到 Cloud Identity Platform 等。

Identity Platform 基于 Firebase Authentication,并增加了多项企业功能,包括多重身份验证、OIDC 和 SAML 单点登录支持、多租户、99.95% 服务等级协议 (SLA) 等等。Identity Platform 与 Firebase Authentication 产品比较页面中也重点介绍了这些差异。这两种产品的功能都大大多于 Users 服务提供的功能。

此模块 21 Codelab 演示了如何将应用的用户身份验证从 Users 服务切换到 Identity Platform 功能,这些功能与模块 20 中演示的功能最为相似。模块 21 还介绍了如何从 App Engine NDB 迁移到 Cloud NDB 以进行 Datastore 访问,重复了模块 2 的迁移。

虽然模块 20 中的代码“宣传”为 Python 2 示例应用,但源代码本身与 Python 2 和 3 兼容,即使在模块 21 中迁移到 Identity Platform(和 Cloud NDB)后,源代码仍然与 Python 2 和 3 兼容。在升级到 Python 3 时,您可以继续使用 Users 服务,因为迁移到 Identity Platform 是可选的。请参阅模块 17 Codelab 和视频,了解如何在升级到第二代运行时(例如 Python 3)的同时继续使用捆绑服务。

本教程包含以下步骤:

  1. 设置/准备工作
  2. 更新配置
  3. 修改应用代码

3. 设置/准备工作

本节介绍如何执行以下操作:

  1. 设置 Cloud 项目
  2. 获取基准示例应用
  3. (重新)部署并验证基准应用
  4. 启用新的 Google Cloud 服务/API

这些步骤可确保您从可正常运行的代码开始,以便将代码迁移到独立 Cloud 服务。

1. 设置项目

如果您已完成模块 20 Codelab,请重复使用同一项目(和代码)。或者,您可以创建一个全新的项目或重复使用另一个现有项目。确保项目具有有效的结算账号,并且已启用 App Engine 应用。找到您的项目 ID,并在本 Codelab 中随时使用它,每当遇到 PROJ_ID 变量时,请使用您的项目 ID。

2. 获取基准示例应用

前提条件之一是拥有一个正常运行的模块 20 App Engine 应用,因此请完成其 Codelab(推荐;上面的链接),或从代码库中复制模块 20 代码。无论您是使用自己的代码还是我们的代码,我们都能在这里开始(“START”)。本 Codelab 将引导您完成迁移,最后获得类似于模块 21 代码库文件夹(“FINISH”)中的代码。

复制模块 20 代码库文件夹。它应类似于以下输出,如果您完成了模块 20 的 Codelab,则可能还会有一个 lib 文件夹:

$ ls
README.md               appengine_config.py     templates
app.yaml                main.py                 requirements.txt

3. (重新)部署并验证基准应用

执行以下步骤来部署模块 20 应用:

  1. 删除 lib 文件夹(如果有),然后运行 pip install -t lib -r requirements.txt 以重新填充该文件夹。如果您同时安装了 Python 2 和 3,可能需要使用 pip2
  2. 确保您已安装初始化 gcloud 命令行工具,并查看其用法
  3. 如果您不想在每次发出 gcloud 命令时都输入 PROJ_ID,请先使用 gcloud config set project PROJ_ID 设置云项目。
  4. 使用 gcloud app deploy 部署示例应用
  5. 确认应用按预期运行,没有出现错误。如果您已完成模块 20 的 Codelab,应用会在顶部显示用户登录信息(用户电子邮件地址、可能的“管理员徽章”和登录/退出按钮),以及最近的访问记录(如下图所示)。

907e64c19ef964f8.png

以常规用户身份登录会导致系统显示用户的电子邮件地址,“登录”按钮也会变为“退出”按钮:

ad7b59916b69a035.png

以管理员用户身份登录会导致用户的电子邮件地址显示在旁边,并带有“(管理员)”字样:

867bcb3334149e4.png

4. 启用新的 Google Cloud API/服务

简介

模块 20 应用使用 App Engine NDB 和 Users API,这些捆绑服务不需要额外设置,但独立的 Cloud 服务需要额外设置,并且更新后的应用将同时使用 Cloud Identity Platform 和 Cloud Datastore(通过 Cloud NDB 客户端库)。此外,我们需要确定 App Engine 管理员用户,因此也需要使用 Cloud Resource Manager API

费用

用户可以根据自己的偏好,通过 Cloud 控制台或命令行(通过 Cloud SDK 的一部分 gcloud 命令)启用 Cloud API。我们先从 Cloud Datastore 和 Cloud Resource Manager API 开始。

通过 Cloud 控制台

在 Cloud 控制台中,前往 API 管理器的“库”页面(确保选择正确的项目),然后使用搜索栏搜索 API。c7a740304e9d35b.png

启用以下 API:

分别找到并点击每个 API 的启用按钮 - 系统可能会提示您提供结算信息。例如,以下是 Resource Manager API 的页面:

fc7bd8f4c49d12e5.png

启用后,该按钮会变为“管理”(通常在几秒钟后):

8eca12d6cc7b45b0.png

以相同的方式启用 Cloud Datastore:

83811599b110e46b.png

从命令行

虽然从控制台中启用 API 在视觉上更直观,但有些人更喜欢使用命令行。此外,您还可以一次性启用任意数量的 API。运行此命令以启用 Cloud Datastore 和 Cloud Resource Manager API,并等待操作完成,如下所示:

$ gcloud services enable cloudresourcemanager.googleapis.com datastore.googleapis.com
Operation "operations/acat.p2-aaa-bbb-ccc-ddd-eee-ffffff" finished successfully.

系统可能会提示您输入结算信息。

上述命令中使用的每个 API 的“网址”称为 API 服务名称,您可以在每个 API 的库页面底部找到这些名称。如果您希望为自己的应用启用其他 Cloud API,可以在相应的 API 页面上找到它们各自的服务名称。此命令会列出您可以启用的 API 的所有服务名称:

gcloud services list --available --filter="name:googleapis.com"

无论是在 Cloud 控制台中还是在命令行中,完成上述步骤后,我们的示例现在都能够访问这些 API。接下来的步骤是启用 Cloud Identity Platform 并进行必要的代码更改。

启用并设置 Cloud Identity Platform(仅限 Cloud 控制台)

Cloud Identity Platform 是一项 Marketplace 服务,因为它会连接到 Google Cloud 之外的资源或依赖于这些资源,例如 Firebase Authentication。目前,您只能通过 Cloud 控制台启用 Marketplace 服务。请按以下步骤操作:

  1. 前往 Cloud Marketplace 中的 Cloud Identity Platform 页面,然后点击该页面上的启用按钮。如果系统提示,请从 Firebase Authentication 升级,这样可以解锁更多功能,例如背景部分中介绍的功能。以下是突出显示启用按钮的 Marketplace 页面:28475f1c9b29de69.png
  2. 启用 Identity Platform 后,系统可能会自动将您转到身份提供商页面。如果不是,请使用此便捷链接前往该页面。fc2d92d42a5d1dd7.png
  3. 启用 Google 身份验证提供程序。如果尚未设置任何提供商,请点击添加提供商,然后选择 Google。返回此界面时,Google 条目应处于启用状态。在本教程中,我们仅使用 Google 作为身份验证提供方,以将 App Engine Users 服务作为轻量级 Google 登录服务进行镜像。在您自己的应用中,您可以启用其他身份验证提供方。
  4. 选择并设置 Google 和其他所需的身份验证提供方后,点击应用设置详情,然后在随即显示的对话框窗口中,复制“Web”标签页上 config 对象中的 apiKeyauthDomain,并将这两者都保存到安全的位置。为什么不全部复制?此对话框中的代码段是硬编码的,并且已过时,因此只需保存最重要的部分,并在我们的代码中使用这些部分,同时更频繁地使用 Firebase Auth。复制这些值并将其保存在安全的地方后,点击关闭按钮,完成所有必要的设置。bbb09dcdd9be538e.png

4. 更新配置

配置方面的更新包括更改各种配置文件,以及在 Cloud Identity Platform 生态系统中创建相当于 App Engine 的内容。

appengine_config.py

  • 如果升级到 Python 3,请删除 appengine_config.py
  • 如果您计划改用 Identity Platform,但使用 Python 2,请勿删除该文件。我们将在稍后的 Python 2 向后移植期间更新它。

requirements.txt

模块 20 的 requirements.txt 文件仅列出了 Flask。对于模块 21,请添加以下软件包:

requirements.txt 的内容现在应如下所示:

flask
google-auth
google-cloud-ndb
google-cloud-resource-manager
firebase-admin

app.yaml

  • 升级到 Python 3 意味着简化 app.yaml 文件。移除除运行时指令之外的所有内容,并将运行时指令设置为当前受支持的 Python 3 版本。该示例目前使用的是 3.10 版。
  • 如果您继续使用 Python 2,则暂时无需在此处采取任何行动。

之前

runtime: python27
threadsafe: yes
api_version: 1

handlers:
- url: /.*
  script: main.app

模块 20 示例应用没有静态文件处理程序。如果您的应用需要,请保持原样。您可以根据需要移除所有脚本处理程序,也可以保留它们以供参考,前提是将它们的句柄更改为 auto,如 app.yaml 迁移指南中所述。进行这些更改后,更新后的 Python 3 app.yaml 简化为:

升级后

runtime: python310

其他配置更新

无论您是继续使用 Python 2 还是移植到 Python 3,如果您有 lib 文件夹,请将其删除。

5. 修改应用代码

此部分介绍了对主要应用文件 main.py 的更新,将 App Engine Users 服务的用法替换为 Cloud Identity Platform。更新主应用后,您将更新 Web 模板 templates/index.html

更新导入和初始化

请按照以下步骤更新导入并初始化应用资源:

  1. 对于导入,请将 App Engine NDB 替换为 Cloud NDB。
  2. 除了 Cloud NDB 之外,还导入 Cloud Resource Manager。
  3. Identity Platform 基于 Firebase Auth,因此请导入 Firebase Admin SDK。
  4. Cloud API 需要使用 API 客户端,因此在初始化 Flask 后立即为 Cloud NDB 初始化 API 客户端。

虽然此处导入了 Cloud Resource Manager 软件包,但我们将在应用初始化的后期阶段使用它。以下是模块 20 中的导入和初始化,以及在实现上述更改后,相应部分应如下所示:

之前

from flask import Flask, render_template, request
from google.appengine.api import users
from google.appengine.ext import ndb

app = Flask(__name__)

升级后

from flask import Flask, render_template, request
from google.auth import default
from google.cloud import ndb, resourcemanager
from firebase_admin import auth, initialize_app

# initialize Flask and Cloud NDB API client
app = Flask(__name__)
ds_client = ndb.Client()

对 App Engine 管理员用户的支持

需要向应用添加两个组件,以支持识别管理员用户:

  • _get_gae_admins() - 整理管理员用户集;调用一次并保存
  • is_admin() - 检查登录用户是否为管理员用户;在任何用户登录时调用

实用函数 _get_gae_admins() 调用 Resource Manager API 来获取当前的 Cloud IAM 允许政策。允许政策定义了向哪些主账号(自然人用户、服务账号等)授予何种角色并强制执行。设置包括:

  • 正在获取云项目 ID (PROJ_ID)
  • 创建 Resource Manager API 客户端 (rm_client)
  • 创建一组(只读)App Engine Admin 角色 (_TARGETS)

Resource Manager 需要 Cloud 项目 ID,因此请导入 google.auth.default() 并调用该函数以获取项目 ID。该调用包含一个看起来像网址但实际上是 OAuth2 权限范围的参数。在云端(例如在 Compute Engine 虚拟机或 App Engine 应用上)运行应用时,系统会提供具有广泛权限的默认服务账号。为遵循最小权限的最佳实践,我们建议您创建自己的用户管理的服务账号

对于 API 调用,最好进一步将应用的范围缩小到正常运行所需的最低级别。我们将要进行的 Resource Manager API 调用是 get_iam_policy(),该调用需要以下某个范围才能运行:

  • https://www.googleapis.com/auth/cloud-platform
  • https://www.googleapis.com/auth/cloud-platform.read-only
  • https://www.googleapis.com/auth/cloudplatformprojects
  • https://www.googleapis.com/auth/cloudplatformprojects.readonly

示例应用只需要对允许政策的只读权限。它不会修改政策,也不需要访问整个项目。这意味着,该应用不需要前三种所需的任何权限。最后一个是必需的,也是我们为示例应用实现的。

函数的正文部分会创建一个空的管理员用户集 (admins),通过 get_iam_policy() 获取 allow_policy,并遍历其所有绑定,专门查找 App Engine Admin 角色:

  • roles/viewer
  • roles/editor
  • roles/owner
  • roles/appengine.appAdmin

对于找到的每个目标角色,该脚本会整理属于该角色的用户,并将这些用户添加到管理员用户的总体集合中。最后,它会返回在此 App Engine 实例的生命周期内找到并缓存的所有管理员用户(以常量 [_ADMINS] 的形式)。我们很快就会看到该来电。

将以下 _get_gae_admins() 函数定义添加到 main.py 中,紧邻实例化 Cloud NDB API 客户端 (ds_client) 的代码下方:

def _get_gae_admins():
    'return set of App Engine admins'
    # setup constants for calling Cloud Resource Manager API
    _, PROJ_ID = default(  # Application Default Credentials and project ID
            ['https://www.googleapis.com/auth/cloudplatformprojects.readonly'])
    rm_client = resourcemanager.ProjectsClient()
    _TARGETS = frozenset((     # App Engine admin roles
            'roles/viewer',
            'roles/editor',
            'roles/owner',
            'roles/appengine.appAdmin',
    ))

    # collate users who are members of at least one GAE admin role (_TARGETS)
    admins = set()                      # set of all App Engine admins
    allow_policy = rm_client.get_iam_policy(resource='projects/%s' % PROJ_ID)
    for b in allow_policy.bindings:     # bindings in IAM allow-policy
        if b.role in _TARGETS:          # only look at GAE admin roles
            admins.update(user.split(':', 1).pop() for user in b.members)
    return admins

当用户登录应用时,会发生以下情况:

  1. 用户登录 Firebase 后,系统会从 Web 模板中进行快速检查。
  2. 当模板中的身份验证状态发生变化时,系统会向 /is_admin 发出 Ajax 样式的 fetch() 调用,该调用的处理程序是下一个函数 is_admin()
  3. Firebase ID 令牌在 POST 正文中传递给 is_admin(),后者会从标头中提取该令牌,并调用 Firebase Admin SDK 对其进行验证。如果用户有效,请提取其电子邮件地址并检查其是否为管理员用户。
  4. 然后,系统会将布尔值结果作为成功的 200 返回给模板。

is_admin() 添加到 main.py 中的 _get_gae_admins() 之后:

@app.route('/is_admin', methods=['POST'])
def is_admin():
    'check if user (via their Firebase ID token) is GAE admin (POST) handler'
    id_token = request.headers.get('Authorization')
    email = auth.verify_id_token(id_token).get('email')
    return {'admin': email in _ADMINS}, 200

这两个函数中的所有代码都需要复制 Users 服务(尤其是其 is_current_user_admin() 函数)提供的功能。模块 20 中的此函数调用完成了所有繁重的工作,这与模块 21 不同,在模块 21 中,我们实现了一个替代解决方案。好消息是,该应用不再依赖于仅限 App Engine 的服务,这意味着您可以将应用迁移到 Cloud Run 或其他服务。此外,您只需在 _TARGETS 中切换到所需的角色,即可更改自有应用的“管理员用户”定义,而 Users 服务则针对 App Engine 管理员角色进行了硬编码。

初始化 Firebase Auth 并缓存 App Engine 管理员用户

我们本可以在顶部(靠近初始化 Flask 应用和创建 Cloud NDB API 客户端的位置)初始化 Firebase Auth,但在定义所有管理员代码之前,没有必要这样做,而现在我们已经定义了所有管理员代码。同样,现在 _get_gae_admins() 已定义,调用它来缓存管理员用户列表。

is_admin() 的函数正下方添加以下代码行:

# initialize Firebase and fetch set of App Engine admins
initialize_app()
_ADMINS = _get_gae_admins()

访问数据模型更新

Visit 数据模型不会发生变化。Datastore 访问需要明确使用 Cloud NDB API 客户端上下文管理器 ds_client.context()。在代码中,这意味着您需要在 Python with 块内将 Datastore 调用封装在 store_visit()fetch_visits() 中。此更新与模块 2 完全相同。进行以下更改:

之前

class Visit(ndb.Model):
    'Visit entity registers visitor IP address & timestamp'
    visitor   = ndb.StringProperty()
    timestamp = ndb.DateTimeProperty(auto_now_add=True)

def store_visit(remote_addr, user_agent):
    'create new Visit entity in Datastore'
    Visit(visitor='{}: {}'.format(remote_addr, user_agent)).put()

def fetch_visits(limit):
    'get most recent visits'
    return Visit.query().order(-Visit.timestamp).fetch(limit)

升级后

class Visit(ndb.Model):
    'Visit entity registers visitor IP address & timestamp'
    visitor   = ndb.StringProperty()
    timestamp = ndb.DateTimeProperty(auto_now_add=True)

def store_visit(remote_addr, user_agent):
    'create new Visit entity in Datastore'
    with ds_client.context():
        Visit(visitor='{}: {}'.format(remote_addr, user_agent)).put()

def fetch_visits(limit):
    'get most recent visits'
    with ds_client.context():
        return Visit.query().order(-Visit.timestamp).fetch(limit)

将用户登录逻辑移至 Web 模板

App Engine Users 服务是服务器端服务,而 Firebase Auth 和 Cloud Identity Platform 主要是客户端服务。因此,模块 20 应用中的大部分用户管理代码都移到了模块 21 Web 模板中。

main.py 中,Web 上下文将五个基本数据片段传递给模板,其中前四个与用户管理相关,并且因用户是否已登录而异:

  • who - 如果用户已登录,则为用户的电子邮件地址;否则为 user
  • admin - 如果已登录的用户是管理员,则显示 (管理员)徽章
  • sign - 显示登录退出按钮
  • link - 点击按钮时显示登录或退出链接
  • visits - 最近访问的网页

之前

@app.route('/')
def root():
    'main application (GET) handler'
    store_visit(request.remote_addr, request.user_agent)
    visits = fetch_visits(10)

    # put together users context for web template
    user = users.get_current_user()
    context = {  # logged in
        'who':   user.nickname(),
        'admin': '(admin)' if users.is_current_user_admin() else '',
        'sign':  'Logout',
        'link':  '/_ah/logout?continue=%s://%s/' % (
                      request.environ['wsgi.url_scheme'],
                      request.environ['HTTP_HOST'],
                  ),  # alternative to users.create_logout_url()
    } if user else {  # not logged in
        'who':   'user',
        'admin': '',
        'sign':  'Login',
        'link':  users.create_login_url('/'),
    }

    # add visits to context and render template
    context['visits'] = visits  # display whether logged in or not
    return render_template('index.html', **context)

所有用户管理功能都将移至 Web 模板,因此我们只剩下访问次数,从而使主处理程序恢复到模块 1 应用中的状态:

升级后

@app.route('/')
def root():
    'main application (GET) handler'
    store_visit(request.remote_addr, request.user_agent)
    visits = fetch_visits(10)
    return render_template('index.html', visits=visits)

更新网站模板

上一部分中的所有更新在模板中是什么样的?主要是将用户管理从应用移至模板中运行的 Firebase Auth,并将我们移入 JavaScript 的所有代码部分移植到 JavaScript。我们看到 main.py 大幅缩减,因此预计 templates/index.html 会有类似的增长。

之前

<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
</head>
<body>
<p>
Welcome, {{ who }} <code>{{ admin }}</code>
<button id="logbtn">{{ sign }}</button>
</p><hr>

<h1>VisitMe example</h1>
<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
    <li>{{ visit.timestamp.ctime() }} from {{ visit.visitor }}</li>
{% endfor %}
</ul>

<script>
document.getElementById("logbtn").onclick = () => {
    window.location.href = '{{ link }}';
};
</script>
</body>
</html>

将整个网页模板替换为以下内容:

升级后

<!doctype html>
<html>
<head>
<title>VisitMe Example</title>

<script type="module">
// import Firebase module attributes
import {
        initializeApp
} from "https://www.gstatic.com/firebasejs/9.10.0/firebase-app.js";
import {
        GoogleAuthProvider,
        getAuth,
        onAuthStateChanged,
        signInWithPopup,
        signOut
} from "https://www.gstatic.com/firebasejs/9.10.0/firebase-auth.js";

// Firebase config:
// 1a. Go to: console.cloud.google.com/customer-identity/providers
// 1b. May be prompted to enable GCIP and upgrade from Firebase
// 2. Click: "Application Setup Details" button
// 3. Copy: 'apiKey' and 'authDomain' from 'config' variable
var firebaseConfig = {
        apiKey: "YOUR_API_KEY",
        authDomain: "YOUR_AUTH_DOMAIN",
};

// initialize Firebase app & auth components
initializeApp(firebaseConfig);
var auth = getAuth();
var provider = new GoogleAuthProvider();
//provider.setCustomParameters({prompt: 'select_account'});

// define login and logout button functions
function login() {
    signInWithPopup(auth, provider);
};

function logout() {
    signOut(auth);
};

// check if admin & switch to logout button on login; reset everything on logout
onAuthStateChanged(auth, async (user) => {
    if (user && user != null) {
        var email = user.email;
        who.innerHTML = email;
        logbtn.onclick = logout;
        logbtn.innerHTML = "Logout";
        var idToken = await user.getIdToken();
        var rsp = await fetch("/is_admin", {
                method: "POST",
                headers: {Authorization: idToken}
        });
        var data = await rsp.json();
        if (data.admin) {
            admin.style.display = "inline";
        }
    } else {
        who.innerHTML = "user";
        admin.style.display = "none";
        logbtn.onclick = login;
        logbtn.innerHTML = "Login";
    }
});
</script>
</head>

<body>
<p>
Welcome, <span id="who"></span> <span id="admin"><code>(admin)</code></span>
<button id="logbtn"></button>
</p><hr>

<h1>VisitMe example</h1>
<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
    <li>{{ visit.timestamp.ctime() }} from {{ visit.visitor }}</li>
{% endfor %}
</ul>

<script>
var who    = document.getElementById("who");
var admin  = document.getElementById("admin");
var logbtn = document.getElementById("logbtn");
</script>
</body>
</html>

此 HTML 正文中包含许多组件,我们来逐一了解。

Firebase 导入

在 HTML 文档的标头中,在网页标题之后,导入所需的 Firebase 组件。Firebase 组件现在分为多个模块,以提高效率。用于初始化 Firebase 的代码是从主 Firebase 应用模块导入的,而用于管理 Firebase 身份验证、Google 作为身份验证提供方、登录和退出以及身份验证状态更改“回调”的函数都是从 Firebase Auth 模块导入的:

<!doctype html>
<html>
<head>
<title>VisitMe Example</title>

<script type="module">
// import Firebase module attributes
import {
        initializeApp
} from "https://www.gstatic.com/firebasejs/9.10.0/firebase-app.js";
import {
        GoogleAuthProvider,
        getAuth,
        onAuthStateChanged,
        signInWithPopup,
        signOut
} from "https://www.gstatic.com/firebasejs/9.10.0/firebase-auth.js";

Firebase 配置

在本教程的 Identity Platform 设置部分中,您之前已保存应用设置详情对话框中的 apiKeyauthDomain。在下一部分中,将这些值添加到 firebaseConfig 变量。注释中提供了指向更详细说明的链接:

// Firebase config:
// 1a. Go to: console.cloud.google.com/customer-identity/providers
// 1b. May be prompted to enable GCIP and upgrade from Firebase
// 2. Click: "Application Setup Details" button
// 3. Copy: 'apiKey' and 'authDomain' from 'config' variable
var firebaseConfig = {
        apiKey: "YOUR_API_KEY",
        authDomain: "YOUR_AUTH_DOMAIN",
};

Firebase 初始化

下一部分将使用此配置信息初始化 Firebase。

// initialize Firebase app & auth components
initializeApp(firebaseConfig);
var auth = getAuth();
var provider = new GoogleAuthProvider();
//provider.setCustomParameters({prompt: 'select_account'});

此设置用于启用将 Google 用作身份验证提供方的功能,并提供了一个已注释掉的选项,用于在浏览器会话中仅注册了一个 Google 账号时也显示账号选择器。换句话说,当您有多个账号时,系统会按预期显示此“账号选择器”:a38369389b7c4c7e.png不过,如果会话中只有一个用户,登录过程会自动完成,无需任何用户互动。(弹出式窗口随即显示,然后消失。)您可以取消注释自定义参数行,强制账号选择器对话框针对一位用户显示(而不是立即登录应用)。如果启用,即使是单用户登录也会显示账号选择器:b75624cb68d94557.png

登录和退出函数

以下代码行构成了用于处理登录或退出按钮点击事件的函数:

// define login and logout button functions
function login() {
    signInWithPopup(auth, provider);
};

function logout() {
    signOut(auth);
};

登录和退出操作

<script> 代码块中的最后一个主要部分是针对每次身份验证更改(登录或退出)调用的函数。

// check if admin & switch to logout button on login; reset everything on logout
onAuthStateChanged(auth, async (user) => {
    if (user && user != null) {
        var email = user.email;
        who.innerHTML = email;
        logbtn.onclick = logout;
        logbtn.innerHTML = "Logout";
        var idToken = await user.getIdToken();
        var rsp = await fetch("/is_admin", {
                method: "POST",
                headers: {Authorization: idToken}
        });
        var data = await rsp.json();
        if (data.admin) {
            admin.style.display = "inline";
        }
    } else {
        who.innerHTML = "user";
        admin.style.display = "none";
        logbtn.onclick = login;
        logbtn.innerHTML = "Login";
    }
});
</script>
</head>

模块 20 中用于确定是发送“用户已登录”模板上下文还是“用户已退出”上下文的代码已迁移到此处。如果用户成功登录,顶部的条件会返回 true,从而触发以下操作:

  1. 系统会设置用户的电子邮件地址以供显示。
  2. 登录按钮会变为退出
  3. 系统会以 Ajax 样式调用 /is_admin,以确定是否显示 (admin) 管理员用户徽章。

当用户退出登录时,系统会执行 else 子句来重置所有用户信息:

  1. 用户名设置为 user
  2. 任何管理员徽章均已移除
  3. 退出按钮已改回登录

模板变量

标题部分结束后,正文部分开始,其中包含的模板变量会被 HTML 元素替换,这些元素会根据需要进行更改:

  1. 显示的用户名称
  2. (admin) 管理员徽章(如适用)
  3. 登录退出按钮
<body>
<p>
Welcome, <span id="who"></span> <span id="admin"><code>(admin)</code></span>
<button id="logbtn"></button>
</p><hr>

最近访问和 HTML 元素变量

最近访问的代码不会更改,而最后的 <script> 块会为登录和退出时发生更改的 HTML 元素设置变量(如上文所述):

<h1>VisitMe example</h1>
<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
    <li>{{ visit.timestamp.ctime() }} from {{ visit.visitor }}</li>
{% endfor %}
</ul>

<script>
var who    = document.getElementById("who");
var admin  = document.getElementById("admin");
var logbtn = document.getElementById("logbtn");
</script>
</body>
</html>

至此,我们已完成从 App Engine NDB 和 Users API 切换到 Cloud NDB 和 Identity Platform 以及升级到 Python 3 所需的应用和 Web 模板更改。恭喜,您已成功创建新的模块 21 示例应用!您可以在 Module 21b repo 文件夹中查看我们的版本。

Codelab 的下一部分是可选的 (*),仅适用于必须继续使用 Python 2 的用户,其中将引导您完成必要的步骤,最终获得可正常运行的 Python 2 模块 21 应用。

6. *Python 2 向后移植

此可选部分适用于正在执行 Identity Platform 迁移但必须继续在 Python 2 运行时上运行的开发者。如果您不担心此问题,请跳过此部分。

如需创建可正常运行的 Python 2 版模块 21 应用,您需要:

  1. 运行时要求:支持 Python 2 的配置文件,以及为避免 Python 3 不兼容性而在主应用中做出的必要更改
  2. 次要库变更:在将一些必需的功能添加到 Resource Manager 客户端库之前,Python 2 已被弃用。因此,您需要寻找替代方法来访问缺失的功能。

我们现在就来执行这些步骤,首先是配置。

恢复 appengine_config.py

在本教程的前面部分,我们引导您删除了 appengine_config.py,因为 Python 3 App Engine 运行时不使用该文件。对于 Python 2,不仅必须保留它,而且还需要更新模块 20 appengine_config.py 以支持使用内置第三方库,即 grpciosetuptools。只要您的 App Engine 应用使用 Cloud 客户端库(例如 Cloud NDB 和 Cloud Resource Manager 的客户端库),就必须使用这些软件包。

您稍后会将这些软件包添加到 app.yaml,但为了让应用能够访问它们,必须调用 setuptools 中的 pkg_resources.working_set.add_entry() 函数。这样,安装在 lib 文件夹中的已复制(自行捆绑或供应商提供的)第三方库便能够与内置库通信。

appengine_config.py 文件进行以下更新,以使这些更改生效:

之前

from google.appengine.ext import vendor

# Set PATH to your libraries folder.
PATH = 'lib'
# Add libraries installed in the PATH folder.
vendor.add(PATH)

仅凭此代码不足以支持使用 setuptoolsgrpcio。还需要添加几行代码,因此请更新 appengine_config.py,使其如下所示:

升级后

import pkg_resources
from google.appengine.ext import vendor

# Set PATH to your libraries folder.
PATH = 'lib'
# Add libraries installed in the PATH folder.
vendor.add(PATH)
# Add libraries to pkg_resources working set to find the distribution.
pkg_resources.working_set.add_entry(PATH)

如需详细了解支持 Cloud 客户端库所需的更改,请参阅迁移捆绑服务文档

app.yaml

appengine_config.py 类似,app.yaml 文件必须恢复为支持 Python 2 的文件。我们先从原始模块 20 app.yaml 开始:

之前

runtime: python27
threadsafe: yes
api_version: 1

handlers:
- url: /.*
  script: main.app

除了前面提到的 setuptoolsgrpcio 之外,还有一个依赖项(与 Identity Platform 迁移没有明确关系)需要使用 Cloud Storage 客户端库该库需要另一个内置的第三方软件包 ssl。在新的 libraries 部分中添加所有这三个软件包,并选择这些软件包的“最新”可用版本,以添加到 app.yaml

升级后

runtime: python27
threadsafe: yes
api_version: 1

handlers:
- url: /.*
  script: main.app

libraries:
- name: grpcio
  version: latest
- name: setuptools
  version: latest
- name: ssl
  version: latest

requirements.txt

对于模块 21,我们向 Python 3 requirements.txt 添加了 Google AuthCloud NDBCloud Resource ManagerFirebase Admin SDK。Python 2 的情况更为复杂:

  • Resource Manager API 提供了示例应用所需的允许政策功能。遗憾的是,Cloud Resource Manager 客户端库最终 Python 2 版本中尚未提供此支持。(仅在 Python 3 版本中提供。)
  • 因此,需要提供一种通过 API 访问此功能的替代方法。解决方案是使用较低级别的 Google API 客户端库与 API 进行通信。如需切换到此客户端库,请将 google-cloud-resource-manager 替换为较低级别的 google-api-python-client 软件包。
  • 由于 Python 2 已停用,因此支持模块 21 的依赖关系图需要将某些软件包锁定到特定版本。即使某些软件包未在 Python 3 app.yaml 中指定,也必须调用它们。

之前

flask

从模块 20 requirements.txt 开始,将其更新为以下内容,以获得有效的模块 21 应用:

升级后

grpcio==1.0.0
protobuf<3.18.0
six>=1.13.0
flask
google-gax<0.13.0
google-api-core==1.31.1
google-api-python-client<=1.11.0
google-auth<2.0dev
google-cloud-datastore==1.15.3
google-cloud-firestore==1.9.0
google-cloud-ndb
google-cloud-pubsub==1.7.0
firebase-admin

随着依赖项的更改,软件包和版本号将在代码库中更新,但在撰写本文时,此 app.yaml 足以让应用正常运行。

其他配置更新

如果您尚未删除此 Codelab 前面部分中的 lib 文件夹,请立即删除。使用新更新的 requirements.txt,发出此熟悉的命令,将这些要求安装到 lib 中:

pip install -t lib -r requirements.txt  # or pip2

如果您在开发系统上同时安装了 Python 2 和 3,则可能需要使用 pip2 而不是 pip

修改应用代码

幸运的是,大部分必需的更改都在配置文件中。应用代码中唯一需要更改的地方是,进行小幅更新,以使用较低级别的 Google API 客户端库(而非 Resource Manager 客户端库)来访问 API。templates/index.html Web 模板无需更新。

更新导入和初始化

将 Resource Manager 客户端库 (google.cloud.resourcemanager) 替换为 Google API 客户端库 (googleapiclient.discovery),如下所示:

之前

from flask import Flask, render_template, request
from google.auth import default
from google.cloud import ndb, resourcemanager
from firebase_admin import auth, initialize_app

升级后

from flask import Flask, render_template, request
from google.auth import default
from google.cloud import ndb
from googleapiclient import discovery
from firebase_admin import auth, initialize_app

面向 App Engine 管理员用户的支持服务

需要在 _get_gae_admins() 中进行一些更改,以支持使用较低级别的客户端库。我们先讨论一下有哪些变化,然后再为您提供所有更新代码。

Python 2 代码需要同时使用从 google.auth.default() 返回的凭据和项目 ID。凭据在 Python 3 中未使用,因此已分配给通用下划线 ( _) 虚拟变量。由于 Python 2 版本需要它,因此请将下划线更改为 CREDS。此外,您将创建 API 服务端点(在概念上与 API 客户端类似),而不是创建 Resource Manager API 客户端,因此我们保留相同的变量名称 (rm_client)。不同之处在于,实例化服务端点需要凭据 (CREDS)。

以下代码反映了这些变化:

之前

_, PROJ_ID = default(  # Application Default Credentials and project ID
        ['https://www.googleapis.com/auth/cloudplatformprojects.readonly'])
rm_client = resourcemanager.ProjectsClient()

升级后

CREDS, PROJ_ID = default(  # Application Default Credentials and project ID
        ['https://www.googleapis.com/auth/cloud-platform'])
rm_client = discovery.build('cloudresourcemanager', 'v1', credentials=CREDS)

另一个区别是,Resource Manager 客户端库返回使用点状属性表示法的允许政策对象,而较低级别的客户端库返回使用方括号 ( [ ]) 的 Python 字典,例如,Resource Manager 客户端库使用 binding.role,而较低级别的库使用 binding['role']。前者还使用“下划线分隔”名称,而较低级别的库则偏好使用“驼峰式命名”名称,并且传递 API 参数的方式略有不同。

这些使用方面的差异如下所示:

之前

allow_policy = rm_client.get_iam_policy(resource='projects/%s' % PROJ_ID)
for b in allow_policy.bindings:     # bindings in IAM allow-policy
    if b.role in _TARGETS:          # only look at GAE admin roles
        admins.update(user.split(':', 1).pop() for user in b.members)

升级后

allow_policy = rm_client.projects().getIamPolicy(resource=PROJ_ID).execute()
for b in allow_policy['bindings']:  # bindings in IAM allow-policy
    if b['role'] in _TARGETS:       # only look at GAE admin roles
        admins.update(user.split(':', 1).pop() for user in b['members'])

将所有这些更改放在一起,将 Python 3 _get_gae_admins() 替换为等效的 Python 2 版本:

def _get_gae_admins():
    'return set of App Engine admins'
    # setup constants for calling Cloud Resource Manager API
    CREDS, PROJ_ID = default(  # Application Default Credentials and project ID
            ['https://www.googleapis.com/auth/cloud-platform'])
    rm_client = discovery.build('cloudresourcemanager', 'v1', credentials=CREDS)
    _TARGETS = frozenset((     # App Engine admin roles
            'roles/viewer',
            'roles/editor',
            'roles/owner',
            'roles/appengine.appAdmin',
    ))

    # collate users who are members of at least one GAE admin role (_TARGETS)
    admins = set()                      # set of all App Engine admins
    allow_policy = rm_client.projects().getIamPolicy(resource=PROJ_ID).execute()
    for b in allow_policy['bindings']:  # bindings in IAM allow-policy
        if b['role'] in _TARGETS:       # only look at GAE admin roles
            admins.update(user.split(':', 1).pop() for user in b['members'])
    return admins

is_admin() 函数不需要任何更新,因为它依赖于已更新的 _get_gae_admins()

至此,将 Python 3 模块 21 应用向后移植到 Python 2 所需的更改已全部完成。恭喜,您已成功更新到第 21 模块的示例应用!您可以在模块 21a 代码库文件夹中找到所有代码。

7. 总结/清理

此 Codelab 的最后几个步骤是确保运行此应用的主账号(用户或服务账号)拥有适当的权限,然后部署应用以确认其按预期运行,并且更改反映在输出中。

能够读取 IAM 允许政策

之前,我们介绍了要被识别为 App Engine 管理员用户所需的四个角色,但现在需要熟悉第五个角色:

  • roles/viewer
  • roles/editor
  • roles/owner
  • roles/appengine.appAdmin
  • roles/resourcemanager.projectIamAdmin(适用于访问 IAM 允许政策的主账号)

借助 roles/resourcemanager.projectIamAdmin 角色,正文可以确定最终用户是否是任何 App Engine 管理员角色的成员。如果未加入 roles/resourcemanager.projectIamAdmin,则对 Cloud Resource Manager API 的调用(用于获取允许政策)将会失败。

无需在此处采取任何明确的操作,因为您的应用将在 App Engine 的默认服务账号下运行,该账号会自动获得此角色的成员资格。即使您在开发阶段使用默认服务账号,我们也强烈建议您创建并使用用户管理的服务账号,该账号具有应用正常运行所需的最低权限。如需向此类服务账号授予会员资格,请运行以下命令:

$ gcloud projects add-iam-policy-binding PROJ_ID --member="serviceAccount:USR_MGD_SVC_ACCT@PROJ_ID.iam.gserviceaccount.com" --role=roles/resourcemanager.projectIamAdmin

PROJ_ID 是云项目 ID,USR_MGD_SVC_ACCT@PROJ_ID.iam.gserviceaccount.com 是您为应用创建的用户管理的的服务账号。此命令会输出项目的更新后 IAM 政策,您可以在其中确认服务账号是否具有 roles/resourcemanager.projectIamAdmin 中的成员资格。如需了解详情,请参阅参考文档。再次强调,您无需在此 Codelab 中发出该命令,但请保存此命令,以便在更新您自己的应用时参考。

部署并验证应用

使用标准 gcloud app deploy 命令将应用上传到云端。部署后,您应该会看到与模块 20 应用几乎完全相同的功能,只是您已成功将 App Engine Users 服务替换为 Cloud Identity Platform(和 Firebase Auth)以进行用户管理:

3a83ae745121d70.png

与模块 20 相比,您会发现一个不同之处:点击“登录”后,系统会显示一个弹出式内容(窗口/广告/etc.),而不是重定向,如下面的一些屏幕截图所示。不过,与模块 20 类似,此行为会因已向浏览器注册的 Google 账号数量而略有不同。

如果浏览器中没有已注册的用户,或者只有一个尚未登录的用户,则会显示一个通用的 Google 登录弹出式窗口:

8437f5f3d489a942.png

如果单个用户已在浏览器中注册,但又在其他位置登录,则不会显示对话框(或对话框会弹出并立即关闭),并且应用会进入已登录状态(显示用户电子邮件地址和退出按钮)。

有些开发者可能希望提供账号选择器,即使只有一个用户也是如此:

b75624cb68d94557.png

如需实现此功能,请按照前面的说明取消对网页模板中 provider.setCustomParameters({prompt: 'select_account'}); 行的注释。

如果有多个用户,系统会弹出账号选择器对话框(见下文)。如果用户尚未登录,系统会提示用户登录。如果已登录,弹出式窗口会消失,应用会进入已登录状态。

c454455b6020d5e4.png

模块 21 的登录状态与模块 20 的界面相同:

49ebe4dcc1eff11f.png

管理员用户登录时也是如此:

44302f35b39856eb.png

与模块 21 不同,模块 20 始终从应用(服务器端代码)访问 Web 模板内容的逻辑。模块 20 的一个缺陷是,当最终用户首次访问应用时,系统会注册一次访问;当用户登录时,系统会注册另一次访问。

对于模块 21,登录逻辑仅在 Web 模板(客户端代码)中进行。无需进行任何必需的服务器端往返,即可确定要显示的内容。对服务器的唯一调用是在最终用户登录后检查管理员用户。这意味着,登录和退出不会注册额外的访问,因此“最近的访问”列表在用户管理操作期间会保持不变。请注意,上述屏幕截图显示的是同一组四次访问,但用户登录次数却不止一次。

模块 20 的屏幕截图展示了此 Codelab 开头的“双重访问 bug”。系统会针对每次登录或退出操作显示单独的访问日志。检查每张屏幕截图的最近访问时间戳,以了解时间顺序。

清理

常规

如果您暂时不想继续操作,建议您停用 App Engine 应用,以免产生结算费用。不过,如果您想进一步测试或实验,App Engine 平台有免费配额,因此只要您不超过该使用层级,就不会产生费用。这是计算费用,但相关 App Engine 服务也可能会产生费用,因此请查看其价格页面了解详情。如果此迁移涉及其他 Cloud 服务,则这些服务会单独计费。在任何一种情况下,如果适用,请参阅下文中的“本 Codelab 特有的问题”部分。

为了完全公开透明,我们在此说明,部署到 Google Cloud 无服务器计算平台(例如 App Engine)会产生少量 build 和存储费用Cloud BuildCloud Storage 都有各自的免费配额。存储该图片会占用部分配额。不过,您可能居住在没有此类免费层的地区,因此请注意存储空间用量,以尽可能减少潜在费用。您应查看的特定 Cloud Storage“文件夹”包括:

  • console.cloud.google.com/storage/browser/LOC.artifacts.PROJECT_ID.appspot.com/containers/images
  • console.cloud.google.com/storage/browser/staging.PROJECT_ID.appspot.com
  • 上述存储链接取决于您的 PROJECT_IDLOCation,例如,如果您的应用托管在美国,则为“us”。

另一方面,如果您不打算继续学习此应用或其他相关迁移 Codelab,并且想要彻底删除所有内容,请关闭您的项目

此 Codelab 特有的

以下列出的服务是此 Codelab 特有的。如需了解详情,请参阅各个产品的文档:

后续步骤

除了本教程之外,还有其他迁移模块重点介绍如何从旧版捆绑服务迁出,您可以考虑使用这些模块,包括:

  • 模块 2:从 App Engine ndb 迁移到 Cloud NDB
  • 模块 7-9:从 App Engine 任务队列(推送任务)迁移到 Cloud Tasks
  • 模块 12-13:从 App Engine Memcache 迁移到 Cloud Memorystore
  • 模块 15-16:从 App Engine Blobstore 迁移到 Cloud Storage
  • 模块 18-19:从 App Engine 任务队列(拉取任务)迁移到 Cloud Pub/Sub

App Engine 不再是 Google Cloud 中唯一的无服务器平台。如果您有一个小型 App Engine 应用或功能有限的应用,并希望将其转换为独立的微服务,或者您希望将单体式应用拆分为多个可重用的组件,那么这些都是考虑迁移到 Cloud Functions 的充分理由。如果容器化已成为应用开发工作流的一部分,尤其是当它包含 CI/CD(持续集成/持续交付或部署)流水线时,请考虑迁移到 Cloud Run。以下模块涵盖了这些使用场景:

  • 从 App Engine 迁移到 Cloud Functions:请参阅模块 11
  • 从 App Engine 迁移到 Cloud Run:请参阅模块 4,了解如何使用 Docker 将应用容器化;或参阅模块 5,了解如何在不使用容器、Docker 知识或 Dockerfile 的情况下完成迁移

您可以选择改用其他无服务器平台,但我们建议您先考虑最适合您的应用和使用情形的选项,然后再进行任何更改。

无论您接下来考虑哪个迁移模块,都可以在 开源代码库中访问所有无服务器迁移站内容(Codelab、视频、源代码 [如有])。该代码库的 README 还提供了有关应考虑哪些迁移以及任何相关的迁移模块“顺序”的指南。

8. 其他资源

下面列出了一些其他资源,供开发者进一步探索本模块或相关迁移模块。您可以在下方提供对此内容的反馈,找到代码链接以及您可能会觉得有用的各种文档。

Codelab 问题/反馈

如果您发现本 Codelab 存在任何问题,请先搜索您的问题,然后再提交。用于搜索和创建新问题的链接:

迁移时可参考的资源

下表中提供了模块 20(开始)和模块 21(完成)的代码库文件夹链接。

Codelab

Python 2

Python 3

模块 20

代码

(不适用)

模块 21(本 Codelab)

代码

代码

在线参考

以下是与本教程相关的资源:

Cloud Identity Platform 和 Cloud Marketplace

Cloud Resource Manager、Cloud IAM、Firebase Admin SDK

App Engine Users、App Engine NDB、Cloud NDB、Cloud Datastore

其他迁移模块参考文档

App Engine 迁移

App Engine 平台

Cloud SDK

其他云信息

视频

许可

此作品已获得 Creative Commons Attribution 2.0 通用许可授权。