使用 Cognito 添加应用登录

AWSBeginner
立即练习

介绍

订单应用需要一种让用户登录的方法。你将创建用户目录和公共应用客户端,让合成用户登录,验证签发的令牌,并在清理前刷新会话。

请先完成 构建并验证订单 API。这个全新环境提供独立的公开后端、临时登录输入和令牌验证示例。保持密码与令牌文件私密。

认证考点

认证 考点 本实验练习
Solutions Architect – Associate (SAA-C03) 任务 1.2 Cognito 登录、令牌验证与会话刷新。
Developer – Associate (DVA-C02) 任务 2.1 Cognito 登录、令牌验证与会话刷新。
Security – Specialty (SCS-C03) 任务 4.1 基础练习:Cognito 登录、令牌验证与会话刷新。

实验概览

概念图:登录客户端通过 Cognito 用户池与公共应用客户端获取、验证和刷新会话令牌;独立的公开健康 API 不会因登录而受到保护。

创建应用用户目录

本步骤中,你将建立用户池、公共应用客户端和已确认的合成用户。

进入工作目录,并使用 umask 077,使新创建的文件仅对你的用户可访问。它影响文件权限,不影响 API 授权规则:

cd /home/labex/project
umask 077

Cognito 用户池是应用的用户目录。池中的用户登录应用;准备好的 IAM CLI 身份则管理云资源。查看上方的 AWS View,比较这些原生资源。

创建本实验的用户池。$(...) 存储返回的池 ID,供后续原生命令使用:

POOL_ID=$(aws cognito-idp create-user-pool \
  --pool-name labex-a04-users \
  --query UserPool.Id \
  --output text)
echo "$POOL_ID"

记录 ID;> 将值写入资源清单文件:

printf '%s\n' "$POOL_ID" > pool-id.txt

应用客户端(app client)标识通过这个池让用户登录的应用。公共应用无法保密保存客户端密钥。创建不带密钥的客户端,并允许本教程使用的用户名/密码流程和刷新令牌流程:

CLIENT_ID=$(aws cognito-idp create-user-pool-client \
  --user-pool-id "$POOL_ID" \
  --client-name orders-client \
  --no-generate-secret \
  --explicit-auth-flows ALLOW_USER_PASSWORD_AUTH ALLOW_REFRESH_TOKEN_AUTH \
  --query UserPoolClient.ClientId \
  --output text)
echo "$CLIENT_ID"
printf '%s\n' "$CLIENT_ID" > client-id.txt

检查安全的客户端配置,不请求秘密值:

aws cognito-idp describe-user-pool-client \
  --user-pool-id "$POOL_ID" \
  --client-id "$CLIENT_ID" \
  --query '{Name:UserPoolClient.ClientName,Flows:UserPoolClient.ExplicitAuthFlows}'

创建演示用户,并禁止发送邮件。这是管理员的用户配置动作,与用户后续登录不同:

aws cognito-idp admin-create-user \
  --user-pool-id "$POOL_ID" \
  --username labex-demo \
  --message-action SUPPRESS \
  --user-attributes Name=email,Value=demo@example.test \
  --query '{Username:User.Username,Status:User.UserStatus}'

初始状态为 FORCE_CHANGE_PASSWORD。仅针对这个临时测试用户,通过 CLI 应用提供的永久密码输入文件。文件具有 Username、Password 和 Permanent 字段;file:// 语法读取这些字段,不会打印密码:

aws cognito-idp admin-set-user-password \
  --user-pool-id "$POOL_ID" \
  --cli-input-json file://demo-password.json

仅检查安全的用户状态:

aws cognito-idp admin-get-user \
  --user-pool-id "$POOL_ID" \
  --username labex-demo \
  --query '{Username:Username,Status:UserStatus,Enabled:Enabled}'

预期看到 labex-demo、CONFIRMED 和 true。在 AWS View 中比较本实验池 ID、orders-client、它没有客户端密钥的配置,以及已确认并启用的用户。以下是实际原生池/客户端/用户状态的示例:

Cognito 用户池、公共应用客户端和已确认的演示用户

无关的 labex-a04-reference 池保持为空。此时尚未请求用户令牌。

登录并读取应用身份

本步骤中,你将为已确认的用户获取实际令牌,并观察错误密码被拒绝。

使用 USER_PASSWORD_AUTH 的 InitiateAuth 验证应用的用户名和密码。它不会通过 CLI 的 IAM 管理员身份授权用户。提供的 sign-in.json 包含本次演示的合成用户名/密码参数。

仅将 AuthenticationResult 存储到私密文件。不要对这个响应使用 cat,因为其中包含 bearer token:

aws cognito-idp initiate-auth --client-id "$CLIENT_ID" --auth-flow USER_PASSWORD_AUTH --auth-parameters file://sign-in.json --query AuthenticationResult --output json > tokens.json

使用 jq 仅显示非秘密响应字段。检查令牌是否存在的表达式返回布尔值,不返回令牌值:

jq '{TokenType,ExpiresIn,TokensPresent:(has("AccessToken") and has("IdToken") and has("RefreshToken"))}' tokens.json

预期看到 Bearer、正数的有效期时长,以及 TokensPresent: true。访问令牌用于授权适当的用户/API 请求,ID 令牌携带应用所需的身份信息,刷新令牌可以获取新的访问令牌和 ID 令牌。它们的用途不同,不能互换。

将访问令牌和 ID 令牌提取到私密的传输文件。jq -r 写出原始字符串,重定向使它们不出现在终端上:

jq -r '.AccessToken' tokens.json > access-token.txt
jq -r '.IdToken' tokens.json > id-token.txt

GetUser 使用访问令牌进行身份验证。只请求在输出中显示用户名:

aws cognito-idp get-user \
  --access-token "$(cat access-token.txt)" \
  --query Username \
  --output text

预期得到 labex-demo。这是 Cognito 对签发令牌的实时响应,不是仅从本地 JSON 文件推断的证据。

尝试提供的错误密码参数,将任何意外的令牌响应重定向到另一个私密文件:

aws cognito-idp initiate-auth --client-id "$CLIENT_ID" --auth-flow USER_PASSWORD_AUTH --auth-parameters file://wrong-sign-in.json --query AuthenticationResult --output json > rejected-login.json

预期看到 NotAuthorizedException: Incorrect username or password。命令有意以非零状态退出;读到这个准确的拒绝后,继续下一步。连接或身份验证传输错误不能证明错误密码被拒绝。不要用这次失败结果覆盖有效令牌。本步骤的自动检查独立验证签发的令牌和实时 GetUser 结果;请手动观察特定的错误密码错误。

应用客户端和已确认用户属于同一个池;成功登录返回令牌。

验证签名令牌并刷新会话

本步骤中,你将验证令牌签名和预期接收方,刷新会话,并区分登录与 API 路由保护。

JWT 包含编码后的头部、声明数据和签名。仅解码不能证明签发者是谁。JWKS 是签发者的公共签名密钥集合;RS256 使用可以通过这些公钥验证的 RSA 签名。

读取提供的普通应用验证示例;其中没有密码或令牌字面值:

cat verify_tokens.py

示例从准备好的工作环境端点获取池的公共 JWKS,验证 RS256 和密钥 ID,再检查签发者、当前时间、token_use 和应用客户端。签名中的签发者使用 Cognito 池身份。访问令牌通过 client_id 标识客户端;ID 令牌使用 aud。使用保存的令牌文件和实际池/客户端 ID 运行它:

/home/labex/project/client-venv/bin/python verify_tokens.py tokens.json "$POOL_ID" "$CLIENT_ID"

预期看到 access 和 id 的安全摘要,两者均包含 RS256 和 current_time_valid: true。示例还会在内存中更改一个签名字节,并在接受声明之前确认修改后的签名被拒绝;不会保存篡改的令牌。访问令牌的 aws.cognito.signin.user.admin scope 允许 Cognito 用户自助服务操作。这个 scope 不是 AWS 管理员访问权限、订单归属规则或自定义业务 scope。

根据原始刷新令牌创建私密的刷新参数。这个操作写入文件,不暴露值:

jq '{REFRESH_TOKEN:.RefreshToken}' tokens.json > refresh-parameters.json

无需再次发送用户密码,获取新的访问令牌和 ID 令牌:

aws cognito-idp initiate-auth --client-id "$CLIENT_ID" --auth-flow REFRESH_TOKEN_AUTH --auth-parameters file://refresh-parameters.json --query AuthenticationResult --output json > refreshed.json

通过相同的公钥和声明检查,验证刷新后的令牌:

/home/labex/project/client-venv/bin/python verify_tokens.py refreshed.json "$POOL_ID" "$CLIENT_ID"

预期得到同一池和客户端的有效访问/ID 令牌摘要。刷新响应不一定包含另一个刷新令牌;在清理之前保留原始私密响应。

仅登录不会改变准备好的公开 API。查找它生成的 ID,并在不带 bearer token 的情况下发送公开健康检查请求:

API_ID=$(aws apigatewayv2 get-apis \
  --query "Items[?Name=='labex-a04'].ApiId | [0]" \
  --output text)
echo "$API_ID"
printf '%s\n' "$API_ID" > api-id.txt
curl -i "http://127.0.0.1:8081/api/$API_ID/health"

预期得到 HTTP 200 和 healthy: true。AWS View 仍显示所有路由的授权为 NONE,且没有订单项目。用户身份验证成功,但 API 尚未要求令牌。下一单元添加 JWT 路由授权;有效令牌仍不会自动证明某个订单属于该用户。

刷新使同一应用客户端的登录会话得以继续,不会改变 API 路由的授权配置,也不会授予某个订单的访问权。比较刷新后的身份摘要和仍公开的健康响应时,应分别判断这些结果。

移除登录和 API 资源

本步骤中,你将移除本实验的登录资源和后端配套资源,同时保留无关的参考池、表和日志。

你的资源清单包括 pool-id.txt 中的 labex-a04-users、client-id.txt 中的 orders-client、用户 labex-demo、api-id.txt 中的 API ID、labex-a04-orders-api、它的函数日志组、labex-a04-orders 和角色的 OrdersData 策略。仅删除这些指定资源。

先移除合成用户,再移除应用客户端和本实验的池:

aws cognito-idp admin-delete-user --user-pool-id "$POOL_ID" --username labex-demo
aws cognito-idp delete-user-pool-client --user-pool-id "$POOL_ID" --client-id "$CLIENT_ID"
aws cognito-idp delete-user-pool --user-pool-id "$POOL_ID"

删除准备好的可工作 API 及其后端资源:

aws apigatewayv2 delete-api --api-id "$API_ID"
aws lambda delete-function --function-name labex-a04-orders-api
aws logs delete-log-group --log-group-name /aws/lambda/labex-a04-orders-api
aws dynamodb delete-table \
  --table-name labex-a04-orders \
  --query TableDescription.TableName \
  --output text
aws iam delete-role-policy --role-name labex-a04-execution --policy-name OrdersData

成功查询原生资源清单;传输失败不能证明资源不存在:

aws cognito-idp list-user-pools --max-results 60 --query 'UserPools[].Name'
aws apigatewayv2 get-apis --query 'Items[].Name'
aws lambda list-functions --query 'Functions[].FunctionName'
aws dynamodb list-tables --query TableNames

预期只剩下参考池和参考表,API/函数列表为空。AWS View 必须显示相同状态,并保留参考项目和 INFO platform reference keep unchanged。准备好的执行角色保留 FunctionLogs。

移除私密的身份验证文件,包括提供的合成密码输入。rm -f 仅删除明确列出的本地文件:

rm -f tokens.json access-token.txt id-token.txt refreshed.json refresh-parameters.json rejected-login.json sign-in.json wrong-sign-in.json demo-password.json

仅移除本地文件不等于服务端会话撤销。这里还删除了合成用户、客户端和池;本实验不声称实现通用的 JWT 注销或撤销机制。在作者清理和 VM 关闭之前,保留无关的参考资源。

总结

你创建了应用用户目录和公共应用客户端,配置了合成用户,完成登录并观察错误密码被拒绝。你验证了实际 RS256 签名以及签发者、客户端、时间和类型声明,读取了实时用户身份,并刷新了访问令牌和 ID 令牌。你区分了 Cognito 用户与 IAM 身份、登录与 API 授权,再移除本实验资源和私密文件,同时保留参考资源。

进一步阅读 AWS 官方资料:用户池令牌的用途; JWT 签名与声明验证; 会话刷新与刷新令牌流程。