1. 概览
您将修改用于将服务部署到 Cloud Run 的默认步骤,以提高安全性,然后了解如何以安全的方式访问已部署的应用。该应用是 Cymbal Eats 应用的“合作伙伴注册服务”,供与 Cymbal Eats 合作的公司用于处理食品订单。
学习内容
只需对将应用部署到 Cloud Run 的最少默认步骤进行一些小幅更改,即可显著提高应用的安全性。您将使用现有应用和部署说明,并更改部署步骤以提高已部署应用的安全性。
然后,您将了解如何授权访问应用和发出授权请求。
本文并非全面介绍应用部署安全性,而是着重介绍您可以在未来的所有应用部署中做出的更改,这些更改只需很少的精力就能提高安全性。
2. 设置和要求
自定进度的环境设置
- 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个。



- 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串。您可以随时更新。
- 项目 ID 在所有 Google Cloud 项目中是唯一的,并且是不可变的(一经设置便无法更改)。Cloud 控制台会自动生成一个唯一字符串;通常情况下,您无需关注该字符串。在大多数 Codelab 中,您都需要引用项目 ID(通常用
PROJECT_ID标识)。如果您不喜欢生成的 ID,可以再随机生成一个 ID。或者,您也可以尝试自己的项目 ID,看看是否可用。完成此步骤后便无法更改该 ID,并且此 ID 在项目期间会一直保留。 - 此外,还有第三个值,即部分 API 使用的项目编号,供您参考。如需详细了解所有这三个值,请参阅文档。
- 接下来,您需要在 Cloud 控制台中启用结算功能,以便使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。若要关闭资源以避免产生超出本教程范围的结算费用,您可以删除自己创建的资源或删除整个项目。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。
激活 Cloud Shell
- 在 Cloud Console 中,点击激活 Cloud Shell
。

如果您以前从未启动过 Cloud Shell,将看到一个中间屏幕(非首屏),描述它是什么。如果是这种情况,请点击继续(您将永远不会再看到它)。一次性屏幕如下所示:

预配和连接到 Cloud Shell 只需花几分钟时间。

这个虚拟机已加载了您需要的所有开发工具。它提供了一个持久的 5GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证。只需使用一个浏览器或 Google Chromebook 即可完成本 Codelab 中的大部分(甚至全部)工作。
在连接到 Cloud Shell 后,您应该会看到自己已通过身份验证,并且相关项目已设置为您的项目 ID:
- 在 Cloud Shell 中运行以下命令以确认您已通过身份验证:
gcloud auth list
命令输出
Credentialed Accounts
ACTIVE ACCOUNT
* <my_account>@<my_domain.com>
To set the active account, run:
$ gcloud config set account `ACCOUNT`
- 在 Cloud Shell 中运行以下命令,以确认 gcloud 命令了解您的项目:
gcloud config list project
命令输出
[core] project = <PROJECT_ID>
如果不是上述结果,您可以使用以下命令进行设置:
gcloud config set project <PROJECT_ID>
命令输出
Updated property [core/project].
环境设置
在本实验中,您将在 Cloud Shell 命令行中运行命令。您通常可以直接复制并粘贴命令,但在某些情况下,您需要将占位值更改为正确的值。
- 设置一个环境变量,用于存储项目 ID,以便在后续命令中使用:
export PROJECT_ID=$(gcloud config get-value project)
export REGION=us-central1
export SERVICE_NAME=partner-registration-service
- 启用将运行应用的 Cloud Run 服务 API、将提供 NoSQL 数据存储的 Firestore API、部署命令将使用的 Cloud Build API,以及将用于在构建时保存应用容器的 Artifact Registry:
gcloud services enable \
run.googleapis.com \
firestore.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com
- 初始化原生模式下的 Firestore 数据库。该命令使用 App Engine API,因此必须先启用该 API。
该命令必须为 App Engine(我们不会使用,但出于历史原因必须创建)指定一个区域,并为数据库指定一个区域。我们将使用 us-central 作为 App Engine 的位置,并使用 nam5 作为数据库的位置。nam5 是美国多区域位置。多区域位置可最大限度地提高数据库的可用性和耐用性。
gcloud services enable appengine.googleapis.com
gcloud app create --region=us-central
gcloud firestore databases create --region=nam5
- 克隆示例应用代码库并前往相应目录
git clone https://github.com/GoogleCloudPlatform/cymbal-eats.git
cd cymbal-eats/partner-registration-service
3. 查看 README
打开编辑器,查看构成应用的文件。查看 README.md,其中介绍了部署此应用所需的步骤。其中一些步骤可能涉及需要考虑的隐式或显式安全决策。您将更改以下部分选择,以提升已部署应用的安全级别,具体如下所述:
第 3 步 - 运行 npm install
了解应用中使用的任何第三方软件的出处和完整性非常重要。管理软件供应链安全与构建任何软件(而不仅仅是部署到 Cloud Run 的应用)相关。本实验侧重于部署,因此未涉及此方面,但您可能需要单独研究此主题。
第 4 步和第 5 步 - 修改和运行 deploy.sh
这些步骤会将应用部署到 Cloud Run,并将大多数选项保留为默认值。您将修改此步骤,以通过以下两种主要方式提高部署的安全性:
- 不允许未经身份验证的访问。在探索期间,允许这样做可能很方便,但这是一个供商业合作伙伴使用的 Web 服务,应始终对用户进行身份验证。
- 指定应用必须使用仅具有必要权限的专用服务账号,而不是可能具有超出所需 API 和资源访问权限的默认服务账号。这被称为最小权限原则,是应用安全性的基本概念。
第 6 步至第 11 步 - 发出示例 Web 请求以验证行为是否正确
由于应用部署现在需要进行身份验证,因此这些请求现在必须包含请求者的身份证明。您将直接从命令行发出请求,而不是更改这些文件。
4. 安全地部署服务
在 deploy.sh 脚本中,我们发现需要进行两项更改:不允许未经身份验证的访问,以及使用具有最低权限的专用服务账号。
您将先创建一个新的服务账号,然后修改 deploy.sh 脚本以引用该服务账号并禁止未经身份验证的访问,接着运行修改后的脚本来部署服务,然后才能运行修改后的 deploy.sh 脚本。
创建服务账号并向其授予对 Firestore/Datastore 的必要访问权限
gcloud iam service-accounts create partner-sa
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:partner-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
--role=roles/datastore.user
修改deploy.sh
修改 deploy.sh 文件,以禁止未经身份验证的访问 (-no-allow-unauthenticated),并为已部署的应用指定新的服务账号 (-service-account)。将 GOOGLE_PROJECT_ID 更正为您自己的项目 ID。
您将删除前两行,并更改其他三行,如下所示。
gcloud run deploy $SERVICE_NAME \
--source . \
--platform managed \
--region ${REGION} \
--no-allow-unauthenticated \
--project=$PROJECT_ID \
--service-account=partner-sa@${PROJECT_ID}.iam.gserviceaccount.com
部署该服务
从命令行中,运行 deploy.sh 脚本:
./deploy.sh
部署完成后,命令输出的最后一行将显示新应用的“服务网址”。将网址保存在环境变量中:
export SERVICE_URL=<URL from last line of command output>
现在,尝试使用 curl 工具从应用中提取订单:
curl -i -X GET $SERVICE_URL/partners
curl 命令的 -i 标志用于指示该命令在输出中包含响应标头。输出的第一行应为:
HTTP/2 403
应用在部署时选择了禁止未经身份验证的请求。此 curl 命令不包含任何身份验证信息,因此会被 Cloud Run 拒绝。实际部署的应用甚至不会运行或接收此请求中的任何数据。
5. 发出经过身份验证的请求
通过发出 Web 请求来调用已部署的应用,现在必须对这些请求进行身份验证,Cloud Run 才能允许它们。通过包含以下形式的 Authorization 标头来验证 Web 请求:
Authorization: Bearer identity-token
身份令牌是由受信任的身份验证提供方签发的短期加密签名编码字符串。在这种情况下,您需要提供未过期且有效的 Google 颁发的身份令牌。
以用户账号身份提出请求
Google Cloud CLI 工具可以为默认经过身份验证的用户提供令牌。运行以下命令,获取您自己账号的身份令牌并将其保存在 ID_TOKEN 环境变量中:
export ID_TOKEN=$(gcloud auth print-identity-token)
默认情况下,Google 签发的身份令牌的有效期为一小时。运行以下 curl 命令,发出之前因未经授权而被拒绝的请求。此命令将包含必要的标头:
curl -i -X GET $SERVICE_URL/partners \
-H "Authorization: Bearer $ID_TOKEN"
命令输出应以 HTTP/2 200 开头,表示请求可接受并正在处理。(如果您等待 1 小时后再次尝试此请求,则会失败,因为令牌已过期。)响应正文位于输出的末尾,在空白行之后:
{"status":"success","data":[]}
目前还没有合作伙伴。
使用目录中的示例 JSON 数据通过两个 curl 命令注册合作伙伴:
curl -X POST \
-H "Authorization: Bearer $ID_TOKEN" \
-H "Content-Type: application/json" \
-d "@example-partner.json" \
$SERVICE_URL/partner
和
curl -X POST \
-H "Authorization: Bearer $ID_TOKEN" \
-H "Content-Type: application/json" \
-d "@example-partner2.json" \
$SERVICE_URL/partner
重复之前的 GET 请求,以查看现在已注册的所有合作伙伴:
curl -i -X GET $SERVICE_URL/partners \
-H "Authorization: Bearer $ID_TOKEN"
您应该会看到包含更多内容的 JSON 数据,其中提供了有关两个已注册合作伙伴的信息。
以未经授权的账号身份发出请求
上一步中经过身份验证的请求之所以成功,不仅是因为它经过了身份验证,还因为经过身份验证的用户(您的账号)获得了授权。也就是说,该账号有权调用应用。并非所有经过身份验证的账号都有权这样做。
之前请求中使用的默认账号已获得授权,因为该账号是创建包含应用的项目的账号,并且默认情况下,该账号有权调用账号中的任何 Cloud Run 应用。如果需要,可以撤消该权限,这在生产应用中是可取的。您现在不会这样做,而是会创建一个未分配任何权限或角色的新服务账号,并使用该账号尝试访问已部署的应用。
- 创建名为
tester的服务账号。
gcloud iam service-accounts create tester
- 您将以与之前获取默认账号的身份令牌大致相同的方式获取此新账号的身份令牌。不过,这需要您的默认账号拥有模拟服务账号的权限。向您的账号授予此权限。
export USER_EMAIL=$(gcloud config list account --format "value(core.account)")
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="user:$USER_EMAIL" \
--role=roles/iam.serviceAccountTokenCreator
- 现在,运行以下命令,将此新账号的身份令牌保存到 TEST_IDENTITY 环境变量中。如果该命令显示错误消息,请等待一两分钟,然后重试。
export TEST_TOKEN=$( \
gcloud auth print-identity-token \
--impersonate-service-account \
"tester@$PROJECT_ID.iam.gserviceaccount.com" \
)
- 像之前一样发出经过身份验证的 Web 请求,但使用此身份令牌:
curl -i -X GET $SERVICE_URL/partners \
-H "Authorization: Bearer $TEST_TOKEN"
命令输出将再次以 HTTP/2 403 开头,因为相应请求虽然已通过身份验证,但未获得授权。新服务账号无权调用此应用。
授权账号
用户或服务账号必须对 Cloud Run 服务拥有 Cloud Run Invoker 角色,才能向该服务发出请求。使用以下命令为测试人员服务账号授予该角色:
export REGION=us-central1
gcloud run services add-iam-policy-binding ${SERVICE_NAME} \
--member="serviceAccount:tester@$PROJECT_ID.iam.gserviceaccount.com" \
--role=roles/run.invoker \
--region=${REGION}
等待一两分钟,让新角色得到更新,然后重复经过身份验证的请求。如果自首次保存以来已过一小时或更长时间,则保存新的 TEST_TOKEN。
curl -i -X GET $SERVICE_URL/partners \
-H "Authorization: Bearer $TEST_TOKEN"
命令输出现在以 HTTP/1.1 200 OK 开头,最后一行包含 JSON 响应。此请求已被 Cloud Run 接受,并由应用处理。
6. 对程序进行身份验证与对用户进行身份验证
您之前发出的经过身份验证的请求都使用了 curl 命令行工具。我们本可以改用其他工具和编程语言。不过,经过身份验证的 Cloud Run 请求无法通过使用纯网页的网络浏览器发出。如果用户点击网页中的链接或点击按钮以提交表单,浏览器将不会添加 Cloud Run 为经过身份验证的请求所需的 Authorization 标头。
Cloud Run 的内置身份验证机制旨在供程序使用,而非最终用户使用。
注意:
Cloud Run 可以托管面向用户的 Web 应用,但此类应用必须将 Cloud Run 设置为允许来自用户 Web 浏览器的未经身份验证的请求。如果应用需要用户身份验证,则必须由应用自行处理,而不是要求 Cloud Run 处理。应用可以采用与 Cloud Run 之外的 Web 应用相同的方式来执行此操作。具体做法不在本 Codelab 的讨论范围内。
您可能已经注意到,到目前为止,示例请求的响应都是 JSON 对象,而不是网页。这是因为此合作伙伴注册服务旨在供程序使用,而 JSON 是一种方便它们使用的格式。接下来,您将编写并运行一个程序来使用这些数据。
通过 Python 程序发送经过身份验证的请求
程序可以通过标准 HTTP Web 请求(但包含 Authorization 标头)向受保护的 Cloud Run 应用发出经过身份验证的请求。对于这些程序,唯一的新挑战是获取有效的未过期身份令牌,并将其放置在该标头中。该令牌将由 Cloud Run 使用 Google Cloud Identity and Access Management (IAM) 进行验证,因此该令牌必须由 IAM 认可的授权机构签发和签名。许多语言都有可供程序使用的客户端库,用于请求签发此类令牌。本示例将使用 Python 客户端库 google.auth。有多个 Python 库可用于发出常规 Web 请求;此示例使用的是热门的 requests 模块。
第一步是安装两个客户端库:
pip install google-auth
pip install requests
用于为默认用户请求身份令牌的 Python 代码如下:
credentials, _ = google.auth.default()
credentials.refresh(google.auth.transport.requests.Request())
identity_token = credentials.id_token
如果您使用的是 Cloud Shell 等命令 shell 或您自己计算机上的标准终端 shell,则默认用户是已在该 shell 中完成身份验证的用户。在 Cloud Shell 中,这通常是登录到 Google 的用户。在其他情况下,它是通过 gcloud auth login 或其他 gcloud 命令进行身份验证的用户。如果用户从未登录过,则不会有默认用户,并且此代码会失败。
对于向另一程序发出请求的程序,您通常不希望使用个人的身份,而是使用发出请求的程序的身份。这就是服务账号可以发挥作用的地方了。您部署了 Cloud Run 服务,并为其指定了专用服务账号,该账号在发出 API 请求(例如向 Cloud Firestore 发出请求)时提供身份。当程序在 Google Cloud 平台上运行时,客户端库会自动使用分配给它的服务账号作为其默认身份,因此相同的程序代码在这两种情况下都能正常运行。
添加了 Authorization 标头的请求的 Python 代码如下所示:
auth_header = {"Authorization": "Bearer " + identity_token}
response = requests.get(url, headers=auth_header)
以下完整的 Python 程序将向 Cloud Run 服务发出经过身份验证的请求,以检索所有已注册的合作伙伴,然后打印其名称和分配的 ID。复制并运行以下命令,将此代码保存到文件 print_partners.py 中。
cat > ./print_partners.py << EOF
def print_partners():
import google.auth
import google.auth.transport.requests
import requests
credentials, _ = google.auth.default()
credentials.refresh(google.auth.transport.requests.Request())
identity_token = credentials.id_token
auth_header = {"Authorization": "Bearer " + identity_token}
response = requests.get("${SERVICE_URL}/partners", headers=auth_header)
parsed_response = response.json()
partners = parsed_response["data"]
for partner in partners:
print(f"{partner['partnerId']}: {partner['name']}")
print_partners()
EOF
您将使用 shell 命令运行此程序。您需要先以默认用户身份进行身份验证,以便程序能够使用这些凭据。运行以下 gcloud auth 命令:
gcloud auth application-default login
按照说明完成登录。然后从命令行运行该程序:
python print_partners.py
输出结果如下所示:
10102: Zippy food delivery
67292: Foodful
该程序的请求已到达 Cloud Run 服务,因为该程序已通过您的身份进行身份验证,而您是此项目的所有者,因此默认情况下有权运行该程序。此程序更常以服务账号的身份运行。在大多数 Google Cloud 产品(例如 Cloud Run 或 App Engine)上运行时,默认身份将是服务账号,而不是个人账号。
7. 恭喜!
恭喜,您已完成此 Codelab!
后续步骤:
探索其他 Cymbal Eats Codelab:
- 使用 Eventarc 触发 Cloud Workflows
- 触发 Cloud Storage 中的事件处理
- 从 Cloud Run 连接到专用 CloudSQL
- 从 Cloud Run 连接到全代管式数据库
- 使用 Identity Aware Proxy (IAP) 保护无服务器应用
- 使用 Cloud Scheduler 触发 Cloud Run 作业
- 保护 Cloud Run 入站流量的安全
- 从 GKE Autopilot 连接到专用 AlloyDB
清理
为避免因本教程中使用的资源导致您的 Google Cloud 账号产生费用,请删除包含这些资源的项目,或者保留项目但删除各个资源。
删除项目
若要避免产生费用,最简单的方法是删除您为本教程创建的项目。