使用 JWT 授权器保护 API 路由

AWSBeginner
立即练习

介绍

订单 API 必须要求用户登录后才能读取和写入,同时保持健康检查公开。你将为两个订单路由添加令牌检查,测试接受与拒绝的请求,并确认被拒绝的调用不会改变业务数据。

请先完成 使用 Cognito 添加应用登录 和 构建并验证订单 API。这个全新环境提供可工作的公开后端和合成身份配套资源,不复用此前的资源或令牌。

相关认证考点

本实验为以下认证考点提供动手练习。

检查公开路由并登录

本步骤中,你将确认当前的公开路由状态,并为预期的应用客户端获取真实访问令牌。

进入工作目录,保持新创建的令牌文件私密:

cd /home/labex/project
umask 077

打开 Terminal 旁的 AWS View,比较 API 请求、函数执行和存储订单。网关请求日志与 Lambda 执行日志相互独立:被拒绝的令牌应在函数运行之前停止请求。

读取安全的配套资源清单。它标识预期的池/客户端、另一个客户端、另一个签发者和无关的参考池:

cat scenario.json

使用 jq -r 选择主要 ID,通过 $(...) 命令替换存储输出:

POOL_ID=$(jq -r '.main.pool' scenario.json)
CLIENT_ID=$(jq -r '.main.client' scenario.json)

按名称查找准备好的 HTTP API,并记录生成的 ID 用于清理:

API_ID=$(aws apigatewayv2 get-apis \
  --query "Items[?Name=='labex-a05'].ApiId | [0]" \
  --output text)
printf '%s\n' "$API_ID" > api-id.txt

检查路由授权类型:

aws apigatewayv2 get-routes \
  --api-id "$API_ID" \
  --query 'Items[].{Route:RouteKey,Authorization:AuthorizationType}'

三个路由最初都使用 NONE。仅有用户目录尚未保护 API。构造工作环境地址:

API_URL="http://127.0.0.1:8081/api/$API_ID"
curl -i "$API_URL/health"

预期得到 HTTP 200 和 healthy: true,AWS View 中有实际的健康检查执行,且没有订单。

通过预期的公共客户端让准备好的合成用户登录,将真实响应私密地存储:

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

提取访问令牌和 ID 令牌,不打印它们:

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

仅使用访问令牌读取实时的应用用户名:

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

预期得到 labex-demo。AWS View 显示准备好的已确认用户和客户端。独立检查验证实际签发的令牌和实时身份,不会替你再次登录。

为两个订单路由要求授权器

本步骤中,你将配置预期的签发者/接收方,并为私有读取和写入应用同一个 JWT 授权器。

JWT 授权器在 API Gateway 调用受保护路由之前检查签名令牌。签发者(issuer)是签名中的 Cognito 池身份。接收方(audience)是预期的应用客户端:存在 aud 时 API Gateway 验证它,否则验证 client_id。根据实际池 ID 构造签发者:

ISSUER="https://cognito-idp.us-east-1.amazonaws.com/$POOL_ID"

使用不带引号的 here-document 标记,让 shell 将两个 ID 展开到普通配置文件中:

cat > jwt-config.json <<JSON
{
  "Issuer": "$ISSUER",
  "Audience": ["$CLIENT_ID"]
}
JSON

创建 JWT 授权器。单引号保留字面值 $request.header.Authorization 身份来源,避免把它展开为 shell 变量:

AUTH_ID=$(aws apigatewayv2 create-authorizer \
  --api-id "$API_ID" \
  --name OrdersUsers \
  --authorizer-type JWT \
  --identity-source '$request.header.Authorization' \
  --jwt-configuration file://jwt-config.json \
  --query AuthorizerId \
  --output text)

查找每个订单路由生成的 ID:

READ_ROUTE_ID=$(aws apigatewayv2 get-routes \
  --api-id "$API_ID" \
  --query "Items[?RouteKey=='GET /orders'].RouteId | [0]" \
  --output text)
WRITE_ROUTE_ID=$(aws apigatewayv2 get-routes \
  --api-id "$API_ID" \
  --query "Items[?RouteKey=='POST /orders'].RouteId | [0]" \
  --output text)

API Gateway 并不会在所有情况下区分访问令牌与 ID 令牌。这些原生 Cognito 密码流程的访问令牌带有 aws.cognito.signin.user.admin;要求这个 scope 会拒绝缺少它的 ID 令牌。这个 scope 指的是 Cognito 用户自助服务访问,不是 AWS 管理权限或订单归属。本实验不创建自定义业务 scope。

为读取路由要求 JWT 和这个 scope:

aws apigatewayv2 update-route \
  --api-id "$API_ID" \
  --route-id "$READ_ROUTE_ID" \
  --authorization-type JWT \
  --authorizer-id "$AUTH_ID" \
  --authorization-scopes aws.cognito.signin.user.admin \
  --query '{Route:RouteKey,Authorization:AuthorizationType,Scopes:AuthorizationScopes}'

为写入路由应用相同要求:

aws apigatewayv2 update-route \
  --api-id "$API_ID" \
  --route-id "$WRITE_ROUTE_ID" \
  --authorization-type JWT \
  --authorizer-id "$AUTH_ID" \
  --authorization-scopes aws.cognito.signin.user.admin \
  --query '{Route:RouteKey,Authorization:AuthorizationType,Scopes:AuthorizationScopes}'

准备好的 $default 阶段使用 AutoDeploy,因此路由更改自动应用。再次读取全部路由类型:

aws apigatewayv2 get-routes \
  --api-id "$API_ID" \
  --query 'Items[].{Route:RouteKey,Authorization:AuthorizationType}'

预期两个订单路由使用 JWT,GET /health 使用 NONE。在 AWS View 中比较实际签发者、预期接收方、路由授权器 ID 和所需 scope。仅创建授权器而不附加到每个私有路由,并不能保护这些路由。

AWS View 示例,显示公开健康检查和受 JWT 保护的订单路由

示例:两个订单路由共用预期的授权器和 scope,健康检查保持公开。你的 VM 中生成的资源 ID 会不同。

授权器在处理程序运行之前接受或拒绝私有请求。

测试接受和拒绝的请求

本步骤中,你将证明预期用户可以实际访问业务,并且缺失或不合适的令牌会在 Lambda 之前被拒绝。

bearer token 对出示它的人进行身份验证。在 Authorization 请求头中从私密传输文件读取令牌。继续使用 curl -i 仅显示响应头/响应体,不要使用详细的请求头日志。发送数量为 4 的有效订单:

curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat access-token.txt)" -H 'Content-Type: application/json' --data '{"id":"signed-order","quantity":4}'

预期得到 HTTP 201 和总额 1100(4 × 250 + 100)。使用访问令牌读取同一个存储订单:

curl -i "$API_URL/orders?id=signed-order" -H "Authorization: Bearer $(cat access-token.txt)"

预期得到 HTTP 200,ID、数量和计算总额相同。独立检查原生项目:

aws dynamodb get-item \
  --table-name labex-a05-orders \
  --key '{"id":{"S":"signed-order"}}' \
  --consistent-read \
  --query Item

现在不带令牌重复读取:

curl -i "$API_URL/orders?id=signed-order"

预期得到 HTTP 401 Unauthorized,响应中没有私有记录。也测试匿名写入:

curl -i -X POST "$API_URL/orders" -H 'Content-Type: application/json' --data '{"id":"reject-missing","quantity":9}'

它也必须返回 401,且不创建项目。私有读取和写入都要求授权。

提供的错误签名测试令牌修改了一个签名字节。即使可见的声明数据看起来合理,也不能信任它:

curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat bad-signature-token.txt)" -H 'Content-Type: application/json' --data '{"id":"reject-signature","quantity":9}'

预期得到 401。为另一个应用客户端真正签发的令牌也不适用于这个 API。读取那个安全的客户端 ID,通过它登录,并私密地提取访问令牌:

WRONG_CLIENT_ID=$(jq -r '.wrong_client' scenario.json)
aws cognito-idp initiate-auth \
  --client-id "$WRONG_CLIENT_ID" \
  --auth-flow USER_PASSWORD_AUTH \
  --auth-parameters file://sign-in.json \
  --query AuthenticationResult \
  --output json > wrong-client.json
jq -r '.AccessToken' wrong-client.json > wrong-client-token.txt
curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat wrong-client-token.txt)" -H 'Content-Type: application/json' --data '{"id":"reject-client","quantity":9}'

预期得到 401:仅签名有效并不意味着匹配配置的接收方。接下来使用准备好的另一个 Cognito 签发者,它有自己的合成用户和公共客户端:

OTHER_POOL_ID=$(jq -r '.other.pool' scenario.json)
OTHER_CLIENT_ID=$(jq -r '.other.client' scenario.json)
aws cognito-idp initiate-auth \
  --client-id "$OTHER_CLIENT_ID" \
  --auth-flow USER_PASSWORD_AUTH \
  --auth-parameters file://sign-in.json \
  --query AuthenticationResult \
  --output json > wrong-issuer.json
jq -r '.AccessToken' wrong-issuer.json > wrong-issuer-token.txt
curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat wrong-issuer-token.txt)" -H 'Content-Type: application/json' --data '{"id":"reject-issuer","quantity":9}'

预期得到 401,因为签名中的签发者与预期池不同。提供的过期测试令牌已签名,exp 位于过去;它测试时间规则,无需等待实时会话过期:

curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat expired-token.txt)" -H 'Content-Type: application/json' --data '{"id":"reject-expired","quantity":9}'

预期得到 401。ID 令牌为这个客户端签名,但缺少路由要求的访问令牌 scope:

curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat id-token.txt)" -H 'Content-Type: application/json' --data '{"id":"reject-id-token","quantity":9}'

预期因 scope 不足得到 403。签名、签发者、接收方和令牌有效期都是必要条件,但无法补足缺失的权限。

检查健康检查仍然公开,并且只存储了有效的业务写入:

curl -i "$API_URL/health"
aws dynamodb scan --table-name labex-a05-orders --query Items

预期健康检查返回 200,且恰好只有总额为 1100 的 signed-order。在 AWS View 中,将实际 API requests 与 CloudWatch Logs 进行比较:授权拒绝具有 401/403 网关结果,没有匹配的函数执行。成功的私有请求具有实际 Lambda 事件和响应,凭证请求头被排除。评估证据是这些运行时/API 结果与原生数据,而非截图或本地成功标记。

实际接受与拒绝 API 请求的示例

示例:有效的订单写入和读取返回 201/200;不合适的令牌返回 401/403。函数执行列表和原生表独立确认了被拒绝请求没有执行业务写入。

移除受保护路由和本实验资源

本步骤中,你将移除本实验的 API、身份配套资源和数据,仅保留无关的参考资源。

你的资源清单包括这个 API 及其授权器、函数/日志组、网关请求日志组、订单表、OrdersData 角色策略、主池(两个客户端和一个用户)、另一个签发者池(一个客户端和一个用户),以及私密输入/响应文件。参考池、表、参考日志和 FunctionLogs 角色策略均与本实验操作无关,必须保留。

删除 API,包括它的授权器、路由、集成和阶段:

aws apigatewayv2 delete-api --api-id "$API_ID"
aws lambda delete-function --function-name labex-a05-orders-api
aws logs delete-log-group --log-group-name /aws/lambda/labex-a05-orders-api
aws logs delete-log-group --log-group-name /aws/apigateway/labex-a05
aws dynamodb delete-table \
  --table-name labex-a05-orders \
  --query TableDescription.TableName \
  --output text
aws iam delete-role-policy --role-name labex-a05-execution --policy-name 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-client \
  --user-pool-id "$POOL_ID" \
  --client-id "$WRONG_CLIENT_ID"
aws cognito-idp delete-user-pool --user-pool-id "$POOL_ID"
aws cognito-idp admin-delete-user --user-pool-id "$OTHER_POOL_ID" --username labex-demo
aws cognito-idp delete-user-pool-client \
  --user-pool-id "$OTHER_POOL_ID" \
  --client-id "$OTHER_CLIENT_ID"
aws cognito-idp delete-user-pool --user-pool-id "$OTHER_POOL_ID"

成功查询清单以证明资源不存在;网络/身份验证错误不能作为清理证据:

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

预期 API/函数列表为空,池和表列表中只有 labex-a05-reference,AWS View 中参考数据/日志得到保留。仅移除列出的私密文件:

rm -f tokens.json access-token.txt id-token.txt wrong-client.json wrong-client-token.txt wrong-issuer.json wrong-issuer-token.txt expired-token.txt future-iat-token.txt future-nbf-token.txt bad-signature-token.txt sign-in.json

仅删除本地文件不是通用的服务端 JWT 撤销。这里还删除了本实验的合成用户/客户端/池和 API。参考配套资源、最终凭证撤销和 VM 关闭,在这些只读资源检查之后由作者清理。

总结

你配置了 Cognito JWT 授权器,并在私有读取和写入路由上要求使用它,同时保持健康检查公开。你测试了真实的接受请求,以及缺失、篡改、错误客户端、错误签发者、过期和 ID 令牌的拒绝情况。你将实际网关结果与原生 Lambda 执行和存储数据关联,再移除本实验的 API/身份/数据资源和私密文件,同时保留参考资源。JWT 授权没有实现按订单划分的归属控制。