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

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

目标

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

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

费用

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

您可使用价格计算器根据您的预计使用情况来估算费用。 Google Cloud 新用户可能有资格申请免费试用

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

准备工作

  1. 登录您的 Google Cloud 账号。如果您是 Google Cloud 新手,请创建一个账号来评估我们的产品在实际场景中的表现。新客户还可获享 $300 赠金,用于运行、测试和部署工作负载。
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Install the Google Cloud CLI.
  4. To initialize the gcloud CLI, run the following command:

    gcloud init
  5. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  6. Install the Google Cloud CLI.
  7. To initialize the gcloud CLI, run the following command:

    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:
            info = jwt.decode(
                assertion,
                certs(),
                algorithms=['ES256'],
                audience=audience()
                )
            return info['email'], info['sub']
        except Exception as e:
            print('Failed to validate assertion: {}'.format(e), file=sys.stderr)
            return None, None
    
    @app.route('/', methods=['GET'])
    def say_hello():
        from flask import request
    
        assertion = request.headers.get('X-Goog-IAP-JWT-Assertion')
        email, id = validate_assertion(assertion)
        page = "<h1>Hello {}</h1>".format(email)
        return page

    本教程后面的了解代码部分详细介绍了此 main.py 文件。

  2. 创建另一个名为 requirements.txt 的文件,并将以下内容粘贴到其中:

    Flask==2.1.2
    cryptography==37.0.2
    python-jose[cryptography]==3.3.0
    requests==2.27.1

    requirements.txt 文件列出了您的应用需要 App Engine 为其加载的所有非标准 Python 库:

    • Flask 是用于应用的 Python Web 框架。

    • cryptography 是一个提供强加密函数的模块。

    • python-jose[cryptography] 提供 JWT 检查和解码功能。

    • 使用 requests 从网站检索数据。

  3. 创建一个名为 app.yaml 的文件,并在其粘贴以下文本:

    runtime: python37

    app.yaml 文件告知 App Engine 您的代码需要哪种语言环境。

了解代码

本部分介绍 main.py 中代码的工作原理。如果您只是想要运行该应用,则可以直接跳至部署应用部分。

以下代码位于 main.py 文件中。当应用收到发送至首页的 HTTP GET 请求时,Flask 框架会调用 say_hello 函数:

@app.route('/', methods=['GET'])
def say_hello():
    from flask import request

    assertion = request.headers.get('X-Goog-IAP-JWT-Assertion')
    email, id = validate_assertion(assertion)
    page = "<h1>Hello {}</h1>".format(email)
    return page

say_hello 函数会从传入请求中获取 IAP 添加的 JWT 断言标头值,并调用函数以验证这一加密签名的值。然后,返回的第一个值(电子邮件)用于该函数创建并返回的极简网页。

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:
        info = jwt.decode(
            assertion,
            certs(),
            algorithms=['ES256'],
            audience=audience()
            )
        return info['email'], info['sub']
    except Exception as e:
        print('Failed to validate assertion: {}'.format(e), file=sys.stderr)
        return None, None

validate_assertion 函数使用第三方 jose 库中的 jwt.decode 的函数来验证断言已正确签名,并从断言中提取载荷信息。该信息是经过身份验证的用户的电子邮件地址以及用户的永久性唯一 ID。 如果无法解码该断言,该函数会为每个值返回 None,并输出消息以记录错误。

验证 JWT 断言需要知道对断言进行签名的实体(本例中为 Google)的公钥证书以及断言面向的目标设备。对于 App Engine 应用,该受众群体是包含 Google Cloud 项目标识信息的字符串。 此函数从它之前的函数中获取这些证书和受众群体字符串。

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

您可以查找 Google Cloud 项目的数字 ID 和名称,并自行将其放入源代码中,但 audience 函数通过查询每个 App Engine 应用都可使用的标准元数据服务为您做到这一点。由于元数据服务位于应用代码的外部,因此该结果保存在返回的全局变量中,而无需在后续调用中查找元数据。

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

App Engine 元数据服务(以及其他 Google Cloud 计算服务的类似元数据服务)看起来像一个网站,通过标准 Web 查询进行查询。不过,它实际上不是外部网站,而是可响应请求并返回关于运行应用的信息的内部功能,因此可以放心地使用 http 请求而非 https 请求。 该服务可用于获取定义 JWT 断言的目标设备所需的当前 Google Cloud 标识符。

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

验证数字签名需要签名者的公钥证书。Google 提供一个可返回当前使用的所有公钥证书的网站。系统会缓存这些结果,以防在同一应用实例中再次需要这些结果。

部署应用

现在,您可以部署应用,然后启用 IAP 以要求用户在访问应用之前进行身份验证。

  1. 在终端窗口中,转到包含 app.yaml 文件的目录,将应用部署到 App Engine:

    gcloud app deploy
    
  2. 出现提示时,选择一个附近的区域。

  3. 系统询问您是否要继续部署操作时,请输入 Y

    您的应用将在几分钟内在互联网上线。

  4. 查看应用:

    gcloud app browse
    

    在输出中,复制应用的网址 web-site-url

  5. 在浏览器窗口中粘贴 web-site-url 以打开应用。

    未显示任何电子邮件,因为您尚未使用 IAP,因此没有任何用户信息发送到应用。

启用 IAP

现在已经有一个 App Engine 实例了,您可以使用 IAP 对其进行保护:

  1. 在 Google Cloud Console 中,转到 Identity-Aware Proxy 页面。

    转到“Identity-Aware Proxy”页面

  2. 由于这是您第一次为此项目启用身份验证选项,您会看到一条消息,要求您必须先配置 OAuth 同意屏幕,然后才能使用 IAP。

    点击配置同意屏幕

  3. 凭据页面的 OAuth 同意屏幕标签页中,填写以下字段:

    • 如果您的帐号属于 Google Workspace 组织,请选择外部,然后点击创建。该应用只能供您明确允许的用户启动。

    • 应用名称字段中,输入 IAP Example

    • 支持电子邮件字段中,输入您的电子邮件地址。

    • 已获授权的网域字段中,输入应用网址的主机名部分,例如 iap-example-999999.uc.r.appspot.com。在字段中输入主机名后按 Enter 键。

    • 应用首页链接字段中,输入应用的网址,例如 https://iap-example-999999.uc.r.appspot.com/

    • 应用隐私权政策链接字段中,使用与首页链接相同的网址以进行测试。

  4. 点击保存。当系统提示您创建凭据时,您可以关闭该窗口。

  5. 在 Cloud Console 中,转到 Identity-Aware Proxy 页面。

    转到“Identity-Aware Proxy”页面

  6. 如需刷新页面,请点击刷新 。该页面会显示您可以保护的资源列表。

  7. IAP 列中,点击应用,对其启用 IAP。

  8. 在浏览器中,再次转到 web-site-url

  9. 您无需使用相应页面,可通过登录屏幕自行验证身份。 您登录时,由于 IAP 中没有允许访问应用的用户列表,因此您无法访问。

将已获授权的用户添加到应用

  1. 在 Cloud Console 中,转到 Identity-Aware Proxy 页面。

    转到“Identity-Aware Proxy”页面

  2. 选中 App Engine 应用的复选框,然后点击添加主账号

  3. 输入 allAuthenticatedUsers,然后选择 Cloud IAP/IAP-Secured Web App User 角色。

  4. 点击保存

现在,Google 可验证的任何用户都可以访问该应用。您还可以根据情况通过仅添加一个或多个用户或组作为主账号,以便进一步限制访问权限:

  • 任何 Gmail 或 Google Workspace 电子邮件地址

  • Google 群组电子邮件地址

  • Google Workspace 域名

访问应用

  1. 在浏览器中,转到 web-site-url

  2. 如需刷新页面,请点击刷新

  3. 在登录屏幕上,使用您的 Google 凭据登录。

    该页面会显示一个包含您的电子邮件地址的“Hello user-email-address”网页。

    如果您看到的页面未发生变化,而现在您已经启用了 IAP,这可能是浏览器存在某个问题,不能完全更新新请求。请关闭所有浏览器窗口,再重新打开,然后重试。

身份验证概念

应用可以通过多种方式对其用户进行身份验证,并仅允许已获授权的用户进行访问。以下各部分列出了常见的身份验证方法,这些方法按应用工作量的多少降序排列。

选项 优点 缺点
应用身份验证
  • 应用可在任何平台(无论是否有互联网连接)上运行
  • 用户无需使用任何其他服务来管理身份验证
  • 应用必须安全地管理用户凭据,防止遭到披露
  • 应用必须为已登录的用户维护会话数据
  • 应用必须提供用户注册、密码更改和密码恢复功能
OAuth2
  • 应用可在任何联网平台上运行,包括开发者工作站
  • 应用不需要用户注册、密码更改或密码恢复功能。
  • 用户信息披露风险委托给其他服务
  • 在应用外部处理新的登录安全措施
  • 用户必须向身份服务注册
  • 应用必须为已登录的用户维护会话数据
IAP
  • 应用无需任何代码即可管理用户、身份验证或会话状态
  • 应用没有可能遭到破解的用户凭据
  • 应用只能在服务支持的平台上运行。 具体来讲,是支持 IAP 的某些 Google Cloud 服务,例如 App Engine。

应用管理的身份验证

使用此方法,应用可单独管理用户身份验证的各个方面。应用必须维护自己的用户凭据数据库并管理用户会话,并且需要提供各类功能,以便管理用户帐号和密码、检查用户凭据,以及在每次经过验证的登录后发布、检查和更新用户会话。下图说明了应用管理的身份验证方法。

应用管理的身份验证流程

如图所示,用户登录后,应用会创建和维护用户会话的相关信息。当用户向应用发出请求时,请求必须包含相应会话信息,应用负责验证此类信息。

这种方法的主要优势在于它是在应用的控制下独立完成的。应用甚至无需联网。主要缺点是应用现在要负责提供所有帐号管理功能,并保护所有敏感凭据数据。

使用 OAuth2 进行外部身份验证

如果不希望在应用内处理所有流程,不妨选择使用 Google 等外部身份服务来处理所有用户帐号的信息和功能,并负责保护敏感凭据。当用户尝试登录应用时,请求会被重定向至身份服务,该服务对用户进行身份验证,然后将请求重定向回应用,同时提供必要的身份验证信息。如需了解详情,请参阅以最终用户身份进行身份验证

下图说明了使用 OAuth2 方法进行外部身份验证的过程。

OAuth2 流程

用户发送请求以访问应用时,图中的流程即开始。应用并没有直接响应,而是将用户的浏览器重定向至 Google 的身份平台,此平台显示可登录 Google 的页面。成功登录后,用户的浏览器将定向至应用。此请求包含当前经过身份验证的用户的信息,供应用进行查询,然后应用会对客户做出响应。

此方法对应用有许多优势。它可以由外部服务承担所有的帐号管理功能和风险,从而提高登录和帐号安全性,而无需更改应用。不过,如上图所示,应用必须能够访问互联网才能使用此方法。应用还负责在用户通过身份验证后管理会话。

Identity-Aware Proxy

本教程介绍的第三种方法是使用 IAP 处理对应用进行任何更改的所有身份验证和会话管理。IAP 会拦截指向应用的所有 Web 请求,阻止任何尚未经过身份验证的请求,并将其它请求在添加用户身份数据后传给应用。

其请求处理流程如下图所示。

IAP 流程

IAP 拦截来自用户的请求,并阻止未经身份验证的请求。经过身份验证的请求会传递到应用,前提是经过身份验证的用户在允许用户列表中。通过 IAP 传递的请求中添加了标头,用于标识发出请求的用户。

该应用不再需要处理任何用户帐号或会话信息。任何需要了解用户唯一标识符的操作都可以直接从每个传入的 Web 请求中获取。但是,这种方法只能用于支持 IAP 的计算服务,例如 App Engine 和负载平衡器。您无法在本地开发机器上使用 IAP。

清理

为避免因本教程中使用的资源导致您的 Google Cloud 帐号产生费用,请删除包含这些资源的项目,或者保留项目但删除各个资源。

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.