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


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

目标

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

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

费用

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

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

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

准备工作

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  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
  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.21
    
    require (
    	cloud.google.com/go/compute/metadata v0.5.0
    	github.com/golang-jwt/jwt v3.2.2+incompatible
    )
    
    require golang.org/x/sys v0.22.0 // 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 等外部身份服务来处理所有用户账号的信息和功能,并负责保护敏感凭据。当用户尝试登录应用时,请求会被重定向至身份服务,该服务对用户进行身份验证,然后将请求重定向回应用,同时提供必要的身份验证信息。如需了解详情,请参阅针对网络服务器应用使用 OAuth 2.0

下图说明了使用 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.