服务之间的身份验证

您可以使用 gRPC 服务中的服务账号在服务之间实现身份验证。本页面将通过一个完整示例演示服务到服务的身份验证,其中包括如何在 gRPC 服务中配置 Extensible Service Proxy (ESP) 以支持经过身份验证的请求,以及如何从 gRPC 客户端调用该服务。

要使任何服务能够对 Cloud Endpoints API 进行经过身份验证的调用,调用服务必须具有服务账号,并且必须在调用中发送身份验证令牌。调用者必须使用 Google ID 令牌或仅由调用者的服务账号签名的自定义 JSON 网络令牌 (JWT)。ESP 将验证 JWT 中的 iss 声明是否与服务配置中的 issuer 设置匹配。ESP 不会检查已授予服务账号的 Identity and Access Management 权限。

在我们的示例中,您将设置和使用最简单的服务到服务身份验证形式,其中客户端使用其 Google Cloud 服务账号生成执行身份验证的 JWT。对于其他身份验证方法而言,方案是类似的,但用于获取有效身份验证令牌的客户端进程取决于使用的身份验证方法。

准备工作

本指南使用的是教程中提供的 Bookstore 示例。

  1. 克隆用于托管 gRPC 示例代码的 Git 代码库:

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git
    
  2. 切换您的工作目录:

    cd python-docs-samples/endpoints/bookstore-grpc/
    
  3. 如果您还没有项目,请按照教程中的说明设置一个。

在该示例中,请使用部署到 Google Kubernetes Engine,尽管对于 Compute Engine 而言,身份验证设置是相同的。

该示例中引用了两个 Google Cloud Platform 项目:

  • 服务提供方项目,即拥有适用于 gRPC 的 Cloud Endpoints 服务的项目。
  • 服务使用方项目,即拥有 gRPC 客户端的项目。

创建使用方服务账号和密钥

如需为使用方项目创建服务账号和密钥,请执行以下操作:

  1. 在 Google Cloud 控制台中,转到“API 和服务”。

    API 和服务

    确保您正位于使用方项目中
  2. 凭据页面上的创建凭据下拉列表中,选择服务账号密钥
  3. 如果要使用的服务账号已存在,请在创建服务账号密钥页面上选择该账号。如果该账号不存在,则在服务账号下拉列表中选择新建服务账号,然后输入账号名称。

    系统将为您创建相应的服务账号 ID。请记下该 ID,因为您在后面部分中需要用到该信息。例如:

    service-account-name@YOUR_PROJECT_ID.iam.gserviceaccount.com
    
  4. 点击角色下拉列表,然后选择以下角色:

    • 服务账号 > 服务账号用户
    • 服务账号 > 服务账号令牌创建者
  5. 务必选中 JSON 密钥类型。

  6. 点击创建。您的服务账号 JSON 密钥文件将下载到本地机器。请注意位置,并确保其安全存储,因为该文件稍后将用于生成令牌。

为服务配置身份验证

请在本部分的所有步骤中使用提供方项目。

在 gRPC API 配置中设置身份验证

ESP 的身份验证在 gRPC API 配置 YAML 文件的 authentication 部分中进行配置。对于该示例服务,包含身份验证的配置位于 api_config_auth.yaml 中。

authentication:
  providers:
  - id: google_service_account
    # Replace SERVICE-ACCOUNT-ID with your service account's email address.
    issuer: SERVICE-ACCOUNT-ID
    jwks_uri: https://www.googleapis.com/robot/v1/metadata/x509/SERVICE-ACCOUNT-ID
  rules:
  # This auth rule will apply to all methods.
  - selector: "*"
    requirements:
      - provider_id: google_service_account

providers 部分指定了要使用的身份验证提供方。在本例中,该部分指定您要将 Google 服务账号用作身份验证提供方。rules 部分指定您需要使用来自此提供方的令牌才能访问服务的所有方法。

在您自己通过克隆代码库而为此文件创建的副本中,执行以下操作:

  • MY_PROJECT_ID 更改为您的提供方项目 ID。
  • authentication 部分中的 SERVICE-ACCOUNT-ID(包含在 issuerjwks_uri 值中)更改为您在上一部分中记录的使用方服务账号 ID。此操作会告知 ESP,您要向通过该特定服务账号提供有效令牌的用户授予对您服务的访问权限。
  • (可选)在 providers 元素下添加 jwt_locations。您可以使用此值指定自定义 JWT 位置。默认 JWT 位置是 Authorization 元数据(带有“Bearer”前缀)和 X-Goog-Iap-Jwt-Assertion 元数据。

保存该文件以供在下一步中使用。

部署配置和服务

以下步骤与 GKE 上的 gRPC 使用入门中的步骤相同:

  1. 将服务配置部署到 Endpoints:即使您已在教程中执行过此操作,也需要这样做,因为这是不同的配置。请记下返回的服务名称:

    gcloud endpoints services deploy api_descriptor.pb api_config_auth.yaml --project PRODUCER_PROJECT
    
  2. 创建容器集群并向该集群进行 kubectl 身份验证(如果您还没有执行此操作)。

  3. 将示例 API 和 ESP 部署到集群。如果您使用的提供方项目和使用方项目不同,请首先确保您已在 gcloud 命令行工具中设置适当的项目:

    gcloud config set project PRODUCER_PROJECT
    

通过 gRPC 客户端调用经过身份验证的方法

最后,在客户端上,您可以使用服务账号密钥生成 JWT 令牌,然后使用该令牌调用经过身份验证的 Bookstore 方法。首先根据生成令牌和运行示例客户端所需的相应 Python 内容进行安装。确保您位于克隆客户端的 python-docs-samples/endpoints/bookstore-grpc 文件夹中,然后运行以下命令:

virtualenv bookstore-env
source bookstore-env/bin/activate
pip install -r requirements.txt

生成 JWT 令牌

在该示例中,Bookstore 使用服务到服务的身份验证,其中调用服务只需通过其服务账号进行身份验证,因此您可以轻松地创建适当令牌,以随我们的请求一起发送。请注意,您还可以设定更严格的服务到服务身份验证要求,即生成的令牌必须通过 Google(使用 Google ID 令牌)进行进一步身份验证。

在本例中,提供的 Python 脚本可以根据先前下载的 JSON 密钥文件,使用虚拟用户 ID 和电子邮件生成令牌。

#!/usr/bin/env python

# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Example of generating a JWT signed from a service account file."""

import argparse
import json
import time

import google.auth.crypt
import google.auth.jwt

"""Max lifetime of the token (one hour, in seconds)."""
MAX_TOKEN_LIFETIME_SECS = 3600


def generate_jwt(service_account_file, issuer, audiences):
    """Generates a signed JSON Web Token using a Google API Service Account."""
    with open(service_account_file) as fh:
        service_account_info = json.load(fh)

    signer = google.auth.crypt.RSASigner.from_string(
        service_account_info["private_key"], service_account_info["private_key_id"]
    )

    now = int(time.time())

    payload = {
        "iat": now,
        "exp": now + MAX_TOKEN_LIFETIME_SECS,
        # aud must match 'audience' in the security configuration in your
        # swagger spec. It can be any string.
        "aud": audiences,
        # iss must match 'issuer' in the security configuration in your
        # swagger spec. It can be any string.
        "iss": issuer,
        # sub and email are mapped to the user id and email respectively.
        "sub": issuer,
        "email": "user@example.com",
    }

    signed_jwt = google.auth.jwt.encode(signer, payload)
    return signed_jwt


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
    )
    parser.add_argument("--file", help="The path to your service account json file.")
    parser.add_argument("--issuer", default="", help="issuer")
    parser.add_argument("--audiences", default="", help="audiences")

    args = parser.parse_args()

    signed_jwt = generate_jwt(args.file, args.issuer, args.audiences)
    print(signed_jwt.decode("utf-8"))

要使用脚本生成令牌,请执行以下操作:

  • 生成 JWT 令牌并将其分配给变量 $JWT_TOKEN

    JWT_TOKEN=$(python jwt_token_gen.py \
        --file=[SERVICE_ACCOUNT_FILE] \
        --audiences=[SERVICE_NAME] \
        --issuer=[SERVICE-ACCOUNT-ID])
    

    其中:

    • [SERVICE_ACCOUNT_FILE] 是已下载的使用方服务账号 JSON 密钥文件。
    • [SERVICE_NAME] 是您将更新后的服务配置部署到 Endpoints 时系统返回的 Bookstore 服务的名称。
    • [SERVICE-ACCOUNT-ID] 是您在生成服务账号时获得的完整使用方服务账号 ID。

发起经过身份验证的 gRPC 调用

最后一步使用 bookstore_client.py,它与教程中使用的客户端相同。为了发起经过身份验证的调用,客户端将 JWT 作为元数据与其方法调用一起传递。

def run(
    host, port, api_key, auth_token, timeout, use_tls, servername_override, ca_path

要运行示例,请执行以下操作:

  1. 使用 kubectl get services 获取已部署 Bookstore 的外部 IP 地址:

    #kubectl get services
    NAME                 CLUSTER-IP      EXTERNAL-IP      PORT(S)           AGE
    echo                 10.11.246.240   104.196.186.92   80/TCP            10d
    endpoints            10.11.243.168   104.196.210.50   80/TCP,8090/TCP   10d
    esp-grpc-bookstore   10.11.254.34    104.196.60.37    80/TCP            1d
    kubernetes           10.11.240.1     <none>           443/TCP           10d
    

    在本例中,服务为 esp-grpc-bookstore,其外部 IP 为 104.196.60.37

  2. 将此 IP 地址分配给变量 EXTERNAL_IP

    EXTERNAL_IP=104.196.60.37
    
  3. 列出 Bookstore 服务中的所有书架:

    python bookstore_client.py --port=80 --host=$EXTERNAL_IP --auth_token=$JWT_TOKEN
    

    该服务会返回当前 Bookstore 中的所有书架。要确认这一点,您可以不提供令牌或在生成 JWT 时指定错误的服务账号 ID。此时命令应失败。

后续步骤