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

1. 概览

无服务器迁移站系列 Codelab(自定进度的实操教程)和相关视频旨在指导 Google Cloud 无服务器开发者完成一次或多次迁移(主要是从旧服务迁移),从而实现应用的现代化改造。这样做可以提高应用的可移植性,为您提供更多选择和灵活性,让您能够集成并访问更广泛的 Cloud 产品,并更轻松地升级到较新版本。虽然本系列最初主要面向的是最早接触 Cloud 的用户(主要是 App Engine(标准环境)开发者),但涵盖的范围非常广泛,涵盖了 Cloud FunctionsCloud Run 等其他无服务器平台,或其他无服务器平台(如适用)。

此 Codelab 旨在向 Python 2 App Engine 开发者展示如何从 App Engine 用户 API/服务迁移到 Cloud Identity Platform (GCIP)。此外,我们还提供从 App Engine NDB 到 Cloud NDB 以访问 Datastore 的隐式迁移(主要在迁移模块 2 中介绍),并升级到 Python 3。

第 20 单元介绍如何在单元 1 的示例应用中添加 Users API。在本单元中,您将学习完成第 20 单元的应用,将其使用情况迁移到 Cloud Identity Platform。

在接下来的实验中

所需条件

调查问卷

您打算如何使用本教程?

<ph type="x-smartling-placeholder"></ph> 仅仔细阅读 阅读并完成练习

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

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

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

<ph type="x-smartling-placeholder"></ph> 新手 中级 熟练

2. 背景

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

Identity Platform 基于 Firebase Authentication,增加了多项企业功能,包括多重身份验证、OIDC 和SAML SSO 支持、多租户、SLA 承诺的 99.95% 可用性等。Identity Platform 和 Firebase Authentication 产品比较页面上也突出显示了这些差异。这两种产品的功能明显多于用户服务所提供的功能。

第 21 单元的 Codelab 演示了如何将应用的用户身份验证从用户服务切换到 Identity Platform 功能,这些功能最能反映第 20 单元中展示的功能。模块 21 还介绍了如何从 App Engine NDB 迁移到 Cloud NDB for Datastore,并重复执行模块 2 迁移。

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

本教程包含以下步骤:

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

3. 设置/准备工作

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

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

以下步骤可确保您从可正常工作的代码入手,该代码已做好迁移到独立 Cloud 服务的准备。

1. 设置项目

如果您已完成第 20 单元 Codelab,请重复使用同一项目(和代码)。或者,创建一个全新的项目或重复使用其他现有项目。确保项目具有有效的结算账号和已启用的 App Engine 应用。在此 Codelab 中,您可以找到项目 ID 并随时使用,然后在遇到 PROJ_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 和 Python 3,则可能需要使用 pip2
  2. 确保您已安装初始化 gcloud 命令行工具,并查看了其使用情况
  3. 如果您不想在发出每个 gcloud 命令后都输入 PROJ_ID,请先使用 gcloud config set project PROJ_ID 设置 Cloud 项目。
  4. 使用 gcloud app deploy 部署示例应用
  5. 确认应用按预期运行,没有出现错误。如果您已完成第 20 单元 Codelab,该应用将在顶部显示用户登录信息(用户电子邮件地址、可能的“管理员徽章”和登录/退出按钮)以及最近的访问活动(如下图所示)。

907e64c19ef964f8

以普通用户的身份登录会显示用户的电子邮件地址,按钮变为“退出”按钮:

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

费用

  • App Engine 和 Cloud Datastore 具有“始终免费”的特性Tier 配额,只要您不超出这些限制,完成本教程就不会产生费用。如需了解详情,另请参阅 App Engine 价格页面Cloud Datastore 价格页面
  • Cloud Identity Platform 的使用费用取决于月活跃用户数 (MAU) 或身份验证验证次数;某些版本的“免费”每种使用模式都可用如需了解详情,请参阅其价格页面。此外,虽然 App Engine 和 Cloud Datastore 需要计费,但只要您不超过无付款方式的每日配额,使用 GCIP 本身就不需要启用结算功能。因此,对于不涉及计费的 Cloud API/服务的 Cloud 项目,请考虑这样做。
  • 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

通过命令行

虽然从控制台启用 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 的库页面底部找到。如果您希望为自己的应用启用其他 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
  2. 启用 Identity Platform 后,系统可能会自动将您定向到 Identity Providers(身份提供商)页面。如果没有,请使用这个方便的链接前往。fc2d92d42a5d1dd7.png
  3. 启用 Google 身份验证提供方。如果未设置任何提供商,请点击 Add a Provider(添加提供商),然后选择 Google。返回此屏幕后,系统应该会启用 Google 条目。Google 是我们在本教程中使用的唯一身份验证提供方,用于将 App Engine 用户服务镜像为轻量级 Google 登录服务。在您自己的应用中,您可以启用其他身份验证提供方。
  4. 选择并设置 Google 和其他所需的身份验证提供方后,点击 Application Setup Details,然后从确保对话框窗口中复制 Web 标签页 config 对象中的 apiKeyauthDomain,并将它们保存在安全的地方。为什么不复制全部内容呢?此对话框中的代码段经过硬编码并注明了日期,因此,您只需保存最重要的部分,然后在代码中使用它们,即可提高并发 Firebase 身份验证的使用率。复制这些值并将其保存在安全的地方后,点击关闭按钮,完成所有必要的设置。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 用户服务替换为 Cloud Identity Platform。更新主应用后,您将需要更新网页模板 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 启动该客户端。

虽然此处导入了 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 allow-policy。allow-policy 会定义并强制执行向哪些主账号(真人用户、服务账号等)授予的角色。设置包括:

  • 正在提取 Cloud 项目 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

示例应用只需要对 allow-policy 的只读权限。它不会修改政策,也不需要访问整个项目。这意味着该应用不需要前三项权限中的任何一项。最后一个函数是必需项,这就是我们将为示例应用实现的功能。

该函数的正文会创建一组空的管理员用户 (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

这两个函数中的所有代码都必须复制用户服务提供的功能,特别是其 is_current_user_admin() 函数。与模块 21 中我们实施替换解决方案不同,模块 20 中的函数调用完成所有繁重工作。好消息是,该应用不再依赖于仅限 App Engine 的服务,这意味着您可以将应用迁移到 Cloud Run 或其他服务。此外,您还可以更改“管理员用户”的定义只需切换到 _TARGETS 中的所需角色即可为自己的应用创建应用;而“用户”服务则经过硬编码,适用于 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()。在代码中,这意味着您将 Datastore 调用同时封装在 Python with 块内的 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)

将用户登录逻辑移至网页模板

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

main.py 中,网络上下文将五条重要数据传递给模板,列出的前四条数据与用户管理相关联,并且会因用户是否已登录:

  • who - 已登录用户的电子邮件地址,否则为 user
  • admin - (管理员)标记(如果已登录的用户是管理员)
  • sign - 显示 Login(登录)或 Logout(退出)按钮
  • 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>

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

之后

<!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. Login(登录)按钮会变为 Logout(退出)。
  3. /is_admin 进行 Ajax 样式调用,以确定是否显示 (admin) 管理员用户徽章。

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

  1. 用户名设为 user
  2. 已移除所有管理员徽章
  3. 退出按钮更改回登录

模板变量

在标题部分结束后,正文会从模板变量开始,这些变量会被 HTML 元素替换,并会根据需要更改:

  1. 显示的用户名
  2. (admin) 管理员徽章(如果适用)
  3. Login(登录)或 Logout(退出)按钮
<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 单元示例应用!您可以在模块 21b 代码库文件夹中查看我们的版本。

此 Codelab 的下一部分是可选的 (*),并且仅适用于应用必须使用 Python 2 的用户,它们将引导您完成获得正常运行的 Python 2 Module 21 应用所需的步骤。

6. *Python 2 向后移植

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

如需创建模块 21 应用的正常运行的 Python 2 版本,您需要以下各项:

  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 部分中添加所有这三项,然后选择“latest”将这些软件包的可用版本复制到 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 提供示例应用所需的 allow-policy 功能。很遗憾,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 和 Python 3,则可能需要使用 pip2 而不是 pip

修改应用代码

幸运的是,大多数所需的更改都发生在配置文件中。应用代码中所需的唯一更改是对 API 进行细微更新,以使用较低级别的 Google API 客户端库(而不是 Resource Manager 客户端库)访问 API。templates/index.html 网站模板无需更新。

更新导入和初始化

将 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。此外,您不应创建 Resource Manager API 客户端,而是创建在概念上与 API 客户端类似的 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 客户端库返回的 allow-policy 对象使用点分属性表示法,而较低级别的客户端库会返回使用方括号 ( [ ]) 的 Python 字典。例如,针对 Resource Manager 客户端库使用 binding.role,针对较低级别的库使用 binding['role']。前者还使用“underscore_Separates”而不是首选“CamelCased”的较低级别的库以及传递 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 Module 21 应用向后移植到 Python 2 所需的更改。恭喜您获得更新后的第 21 单元示例应用!您可以在 Module 21a repo folder 中找到所有代码。

7. 摘要/清理

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

能够读取 IAM allow-policy

之前,我们向您介绍了被视为 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 获取 allow-policy。

无需在此处执行任何明确操作,因为您的应用将在 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 是 Cloud 项目 ID,USR_MGD_SVC_ACCT@PROJ_ID.iam.gserviceaccount.com 是您为应用创建的用户管理的服务账号。此命令会为您的项目输出更新后的 IAM 政策,您可以在其中确认服务账号具有 roles/resourcemanager.projectIamAdmin 成员资格。如需了解详情,请参阅参考文档。再次强调,您无需在此 Codelab 中发出该命令,但可以将其保存作为参考,对您自己的应用进行现代化改造。

部署并验证应用

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

3a83ae745121d70

与模块 20 相比,您会发现一个不同之处在于,点击“登录”后会看到弹出式窗口,而不是重定向,如下面的一些屏幕截图所示。不过,与模块 20 一样,该行为会因浏览器注册的 Google 账号数量而略有不同。

如果没有用户在该浏览器中注册,或者只有单个用户尚未登录,则会显示一个常规的“Google 登录”弹出式窗口:

8437f5f3d489a942

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

有些开发者可能想要提供账号选择器,即便是针对单个用户:

b75624cb68d94557.png

若要实现这一点,请取消对网站模板中的 provider.setCustomParameters({prompt: 'select_account'}); 行的注释,如前所述。

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

c454455b6020d5e4.png

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

49ebe4dcc1eff11f

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

44302f35b39856eb

与模块 21 不同,模块 20 始终从应用(服务器端代码)访问网络模板内容的逻辑。模块 20 的一个缺陷是,当最终用户第一次点击应用时会记录一次访问,而当用户登录时会有另一次访问。

对于模块 21,登录逻辑仅在网页模板(客户端代码)中执行。您无需通过服务器端检查即可确定要显示的内容。系统对服务器执行的唯一调用是在最终用户登录后检查管理员用户。这意味着登录和退出不会记录额外的访问,因此针对用户管理操作,最近的访问列表保持不变。请注意,上面的屏幕截图显示了多个用户登录的同一组四次访问。

第 20 单元的屏幕截图展示了“双重访问 bug”。系统会针对每个登录或退出操作分别显示访问日志。检查最近访问的时间戳,找出每个显示时间顺序的屏幕截图。

清理

常规

如果您目前已完成,我们建议您停用 App Engine 应用,以免产生费用。不过,如果您希望测试或实验更多内容,App Engine 平台有免费配额,因此只要您不超过该使用量水平,您就不必支付费用。这只是计算费用,但相关 App Engine 服务可能也会产生费用,因此请查看其价格页面了解详情。如果此迁移涉及其他 Cloud 服务,这些服务单独计费。无论是哪种情况(如适用),请参阅“此 Codelab 的具体说明”部分。

为了全面披露,部署到像 App Engine 这样的 Google Cloud 无服务器计算平台会产生少量构建和存储费用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_IDLOC类型,例如“us”。

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

此 Codelab 的具体内容

下列服务是此 Codelab 独有的服务。有关详情,请参阅各个产品的文档:

  • App Engine Datastore 服务由 Cloud Datastore(Datastore 模式的 Cloud Firestore)提供,该服务也有一个免费层级;如需了解详情,请参阅其价格页面
  • Cloud Identity Platform 的使用在一定程度上是“免费”的具体取决于您使用的 Google Cloud 控制台服务。如需了解详情,请参阅其价格页面
  • Cloud Resource Manager API 的大部分使用免费,详见价格页面

后续步骤

除了本教程外,其他侧重于摆脱旧版捆绑式服务的迁移模块包括:

  • 第 2 单元:从 App Engine ndb 迁移到 Cloud NDB
  • 模块 7-9:从 App Engine 任务队列(推送任务)迁移到 Cloud Tasks
  • 模块 12-13:从 App Engine Memcache 迁移到 Cloud Memorystore
  • 第 15-16 单元:从 App Engine Blob 存储区迁移到 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 的情况下实现容器化

您可以自行选择是否切换到其他无服务器平台,我们建议您先考虑最适合您的应用和用例的方案,然后再做任何更改。

无论您接下来考虑使用哪种迁移模块,都可以通过其开源代码库访问所有 Serverless Migration Station 内容(Codelab、视频、源代码 [如果有])。代码库的 README 还提供了有关应考虑哪些迁移以及任何相关“顺序”的指南。迁移模块

8. 其他资源

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

Codelab 问题/反馈

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

迁移时可参考的资源

下表列出了模块 20(START)和模块 21 (FINISH) 对应的代码库文件夹的链接。

Codelab

Python 2

Python 3

模块 20

代码

(不适用)

第 21 单元(此 Codelab)

代码

代码

在线参考

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

Cloud Identity Platform 和 Cloud Marketplace

Cloud Resource Manager、Cloud IAM、Firebase Admin SDK

App Engine 用户、App Engine NDB、Cloud NDB、Cloud Datastore

其他迁移模块参考文档

App Engine 迁移

App Engine 平台

Cloud SDK

其他 Cloud 信息

视频

许可

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