使用 Python 版 Cloud Identity-Aware Proxy 对用户进行身份验证

在 Google Cloud 托管平台(例如 App Engine)上运行的应用可以使用 Identity-Aware Proxy (IAP) 控制对自身的访问权限,免于管理用户身份验证和会话。IAP 不仅可以控制对应用的访问权限,还可以提供关于经过身份验证的用户的信息,包括其电子邮件地址和以新的 HTTP 标头形式向应用提供的永久性标识符。

目标

  • 要求 App Engine 应用的用户使用 IAP 验证自己的身份。

  • 在应用中访问用户的身份,以显示当前用户的已验证的电子邮件地址。

费用

在本文档中,您将使用 Google Cloud的以下收费组件:

如需根据您的预计使用情况来估算费用,请使用价格计算器

新 Google Cloud 用户可能有资格申请免费试用

完成本文档中描述的任务后,您可以通过删除所创建的资源来避免继续计费。如需了解详情,请参阅清理

准备工作

  1. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  2. 安装 Google Cloud CLI。

  3. 配置 gcloud CLI 以使用您的联合身份。

    如需了解详情,请参阅使用联合身份登录 gcloud CLI

  4. 如需初始化 gcloud CLI,请运行以下命令:

    gcloud init

背景

本教程使用 IAP 对用户进行身份验证。这只是多种可能方法中的一种。 如需详细了解对用户进行身份验证的各种方法,请参阅身份验证概念部分。

Hello user-email-address 应用

本教程的应用是一个非常小的 Hello World App Engine 应用,具有一个非典型功能:它不显示“Hello World”,而显示“Hello user-email-address”,其中 user-email-address 是经过身份验证的用户的电子邮件地址。

此功能可以通过检查 IAP 向每个 Web 请求添加的身份验证信息并将其传递给您的应用来实现。到达您的应用的每个 Web 请求中都添加了三个新的请求标头。前两个标头是纯文本字符串,可用于标识用户。第三个标头是包含相同信息的经过加密签名的对象。

  • X-Goog-Authenticated-User-Email:用户的电子邮件地址可识别他们。 如果可能,应用应尽量避免存储个人信息。此应用不存储任何数据;它只将其返回给用户。

  • X-Goog-Authenticated-User-Id:Google 分配的此用户 ID 不会显示用户的相关信息,但能让应用知道登录的用户是回访用户。

  • X-Goog-Iap-Jwt-Assertion:除了互联网 Web 请求外,您还可以配置 Google Cloud 应用绕过 IAP,接受来自其他云应用的 Web 请求。如果某个应用采用这一配置,则此类请求中有可能包含伪造的标头。 为应对这一问题,您可以使用并验证经过加密签名的标头,确认该信息来自 Google,而不用前面提到的两个纯文本标头。用户的电子邮件地址和永久性用户 ID 都是此签名标头的一部分。

如果您确定应用已配置为只允许互联网 Web 请求对其进行访问,并且没有人可以停用该应用的 IAP 服务,则检索唯一用户 ID 只需要一行代码:

user_id = request.headers.get('X-Goog-Authenticated-User-ID')

不过,灵活的应用应当预见可能出现的问题,包括意外配置或环境问题,因此建议您创建一个可使用并验证加密签名标头的函数。 该标头的签名无法伪造,并且在验证后可用于返回身份验证信息。

编写源代码

  1. 使用文本编辑器创建名为 main.py 的文件,并在其中粘贴以下代码:

    import sys
    
    from flask import Flask
    app = Flask(__name__)
    
    CERTS = None
    AUDIENCE = None
    
    
    def certs():
        """Returns a dictionary of current Google public key certificates for
        validating Google-signed JWTs. Since these change rarely, the result
        is cached on first request for faster subsequent responses.
        """
        import requests
    
        global CERTS
        if CERTS is None:
            response = requests.get(
                'https://www.gstatic.com/iap/verify/public_key'
            )
            CERTS = response.json()
        return CERTS
    
    
    def get_metadata(item_name):
        """Returns a string with the project metadata value for the item_name.
        See https://cloud.google.com/compute/docs/storing-retrieving-metadata for
        possible item_name values.
        """
        import requests
    
        endpoint = 'http://metadata.google.internal'
        path = '/computeMetadata/v1/project/'
        path += item_name
        response = requests.get(
            '{}{}'.format(endpoint, path),
            headers={'Metadata-Flavor': 'Google'}
        )
        metadata = response.text
        return metadata
    
    
    def audience():
        """Returns the audience value (the JWT 'aud' property) for the current
        running instance. Since this involves a metadata lookup, the result is
        cached when first requested for faster future responses.
        """
        global AUDIENCE
        if AUDIENCE is None:
            project_number = get_metadata('numeric-project-id')
            project_id = get_metadata('project-id')
            AUDIENCE = '/projects/{}/apps/{}'.format(
                project_number, project_id
            )
        return AUDIENCE
    
    
    def validate_assertion(assertion):
        """Checks that the JWT assertion is valid (properly signed, for the
        correct audience) and if so, returns strings for the requesting user's
        email and a persistent user ID. If not valid, returns None for each field.
        """
        from jose import jwt
    
        try