利用 Go 对用户进行身份验证


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

目标

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

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

费用

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

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

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

准备工作

  1. 登录您的 Google Cloud 账号。如果您是 Google Cloud 新手,请创建一个账号来评估我们的产品在实际场景中的表现。新客户还可获享 $300 赠金,用于运行、测试和部署工作负载。
  2. 在 Google Cloud Console 中的项目选择器页面上,选择或创建一个 Google Cloud 项目

    转到“项目选择器”

  3. 安装 Google Cloud CLI。
  4. 如需初始化 gcloud CLI,请运行以下命令:

    gcloud init
  5. 在 Google Cloud Console 中的项目选择器页面上,选择或创建一个 Google Cloud 项目

    转到“项目选择器”

  6. 安装 Google Cloud CLI。
  7. 如需初始化 gcloud CLI,请运行以下命令:

    gcloud init
  8. 准备开发环境

设置项目

  1. 在终端窗口中,将示例应用代码库克隆到本地机器:

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git
  2. 转到包含示例代码的目录:

    cd golang-samples/getting-started/authenticating-users

背景

本教程使用 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 只需要一行代码:

userID := r.Header.Get("X-Goog-Authenticated-User-ID")

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

了解代码

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

  • go.mod 文件定义了 Go 模块及其依赖的模块。

    module github.com/GoogleCloudPlatform/golang-samples/getting-started/authenticating-users
    
    go 1.19
    
    require (
    	cloud.google.com/go/compute/metadata v0.2.3
    	github.com/golang-jwt/jwt v3.2.2+incompatible
    )
    
    require cloud.google.com/go/compute v1.19.1 // indirect
    
  • app.yaml 文件告知 App Engine 您的代码需要哪种语言环境。

    runtime: go112
  • 应用首先导入软件包并定义 main 函数。main 函数会注册索引处理程序并启动 HTTP 服务器。

    
    // The authenticating-users program is a sample web server application that
    // extracts and verifies user identity data passed to it via Identity-Aware
    // Proxy.
    package main
    
    import (
    	"encoding/json"
    	"fmt"
    	"log"
    	"net/http"
    	"os"
    	"time"
    
    	"cloud.google.com/go/compute/metadata"
    	"github.com/golang-jwt/jwt"
    )
    
    // app holds the Cloud IAP certificates and audience field for this app, which
    // are needed to verify authentication headers set by Cloud IAP.
    type app struct {
    	certs map[string]string
    	aud   string
    }
    
    func main() {
    	a, err := newApp()
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	http.HandleFunc("/", a.index)
    
    	port := os.Getenv("PORT")
    	if port == "" {
    		port = "8080"
    		log.Printf("Defaulting to port %s", port)
    	}
    
    	log.Printf("Listening on port %s", port)
    	if err := http.ListenAndServe(":"+port, nil); err != nil {
    		log.Fatal(err)
    	}
    }
    
    // newApp creates a new app, returning an error if either the Cloud IAP
    // certificates or the app's audience field cannot be obtained.
    func newApp() (*app, error) {
    	certs, err := certificates()
    	if err != nil {
    		return nil, err
    	}
    
    	aud, err := audience()
    	if err != nil {
    		return nil, err
    	}
    
    	a := &app{
    		certs: certs,
    		aud:   aud,
    	}
    	return a, nil
    }
    
  • index 函数会从传入请求中获取 IAP 添加的 JWT 断言标头值,并调用 validateAssertion 函数以验证这一加密签名的值。然后,该电子邮件地址会用于极简网页响应。

    
    // index responds to requests with our greeting.
    func (a *app) index(w http.ResponseWriter, r *http.Request) {
    	if r.URL.Path != "/" {
    		http.NotFound(w, r)
    		return
    	}
    
    	assertion := r.Header.Get("X-Goog-IAP-JWT-Assertion")
    	if assertion == "" {
    		fmt.Fprintln(w, "No Cloud IAP header found.")
    		return
    	}
    	email, _, err := validateAssertion(assertion, a.certs, a.aud)
    	if err != nil {
    		log.Println(err)
    		fmt.Fprintln(w, "Could not validate assertion. Check app logs.")
    		return
    	}
    
    	fmt.Fprintf(w, "Hello %s\n", email)
    }
    
  • validateAssertion 函数会验证断言是否已正确签名,并返回关联的电子邮件地址和用户 ID。

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

    
    // validateAssertion validates assertion was signed by Google and returns the
    // associated email and userID.
    func validateAssertion(assertion string, certs map[string]string, aud string) (email string, userID string, err error) {
    	token, err := jwt.Parse(assertion, func(token *jwt.Token) (interface{}, error) {
    		keyID := token.Header["kid"].(string)
    
    		_, ok := token.Method.(*jwt.SigningMethodECDSA)
    		if !ok {
    			return nil, fmt.Errorf("unexpected signing method: %q", token.Header["alg"])
    		}
    
    		cert := certs[keyID]
    		return jwt.ParseECPublicKeyFromPEM([]byte(cert))
    	})
    
    	if err != nil {
    		return "", "", err
    	}
    
    	claims, ok := token.Claims.(jwt.MapClaims)
    	if !ok {
    		return "", "", fmt.Errorf("could not extract claims (%T): %+v", token.Claims, token.Claims)
    	}
    
    	if claims["aud"].(string) != aud {
    		return "", "", fmt.Errorf("mismatched audience. aud field %q does not match %q", claims["aud"], aud)
    	}
    	return claims["email"].(string), claims["sub"].(string), nil
    }
    
  • 您可以查找 Google Cloud 项目的数字 ID 和名称,并自行将其放入源代码中,但 audience 函数会通过查询每个 App Engine 应用都可使用的标准元数据服务为您做到这一点。由于元数据服务在应用代码的外部,因此该结果保存在返回的全局变量中,而无需在后续调用中查找元数据。

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

    
    // audience returns the expected audience value for this service.
    func audience() (string, error) {
    	projectNumber, err := metadata.NumericProjectID()
    	if err != nil {
    		return "", fmt.Errorf("metadata.NumericProjectID: %w", err)
    	}
    
    	projectID, err := metadata.ProjectID()
    	if err != nil {
    		return "", fmt.Errorf("metadata.ProjectID: %w", err)
    	}
    
    	return "/projects/" + projectNumber + "/apps/" + projectID, nil
    }
    
  • 验证数字签名需要签名者的公钥证书。Google 提供一个可返回当前使用的所有公钥证书的网站。系统会缓存这些结果,以防在同一应用实例中再次需要这些结果。

    
    // certificates returns Cloud IAP's cryptographic public keys.
    func certificates() (map[string]string, error) {
    	const url = "https://www.gstatic.com/iap/verify/public_key"
    	client := http.Client{
    		Timeout: 5 * time.Second,
    	}
    	resp, err := client.Get(url)
    	if err != nil {
    		return nil, fmt.Errorf("Get: %w", err)
    	}
    
    	var certs map[string]string
    	dec := json.NewDecoder(resp.Body)
    	if err := dec.Decode(&certs); err != nil {
    		return nil, fmt.Errorf("Decode: %w", err)
    	}
    
    	return certs, nil
    }
    

部署应用

现在,您可以部署应用,然后启用 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 控制台中,转到 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. 在 Google Cloud 控制台中,转到 Identity-Aware Proxy 页面。

    转到“Identity-Aware Proxy”页面

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

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

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

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

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

  1. 在 Google Cloud 控制台中,转到 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 等外部身份服务来处理所有用户账号的信息和功能,并负责保护敏感凭据。当用户尝试登录应用时,请求会被重定向至身份服务,该服务对用户进行身份验证,然后将请求重定向回应用,同时提供必要的身份验证信息。如需了解详情,请参阅针对 Web 服务器应用使用 OAuth 2.0

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

OAuth2 流程

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

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

Identity-Aware Proxy

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

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

IAP 流程

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

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

清理

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

  1. 在 Google Cloud 控制台中,进入管理资源页面。

    转到“管理资源”

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关闭以删除项目。