amazon-web-services - 如何通过特定用户的 Bearer 令牌访问与 S3 Bucket 连接的 AWS CloudFront(JWT 自定义身份验证)
问题描述
我正在使用无服务器框架将无服务器堆栈部署到 AWS。我的堆栈由一些 lambda 函数、DynamoDB 表和 API 网关组成。
我使用所谓的lambda authorizer来保护 API Gateway 。另外,我有一个可以生成令牌的自定义独立自托管 Auth 服务。
所以场景是用户可以从这个服务请求一个令牌(它是托管在 Azure 上的 IdentityServer4),然后用户可以使用不记名令牌向 API 网关发送请求,这样 API 网关将要求 lambda 授权方生成 iam 角色,如果令牌是正确的。所有这些都是有效的并且按预期工作。
这是我的 serverless.yml 中 lambda 授权器定义的示例,以及我如何使用它来保护其他 API 网关端点:(您可以看到 addUserInfo 函数具有使用自定义授权器保护的 API)
functions:
# =================================================================
# API Gateway event handlers
# ================================================================
auth:
handler: api/auth/mda-auth-server.handler
addUserInfo:
handler: api/user/create-replace-user-info.handler
description: Create Or Replace user section
events:
- http:
path: user
method: post
authorizer:
name: auth
resultTtlInSeconds: ${self:custom.resultTtlInSeconds}
identitySource: method.request.header.Authorization
type: token
cors:
origin: '*'
headers: ${self:custom.allowedHeaders}
现在我想扩展我的 API,所以我将允许用户添加图像,所以我遵循了这种方法。因此,在这种方法中,用户将启动所谓的签名 S3 URL,我可以使用此 S3 签名 URL 将图像放入我的存储桶。
此外,S3 存储桶不可公开访问,而是连接到 CloudFront 分配。现在我错过了这里的东西,我无法理解如何保护我的图像。无论如何,我可以使用我的自定义身份验证服务保护 CloudFront CDN 中的图像,以便拥有有效令牌的用户可以访问这些资源?如何使用自定义身份验证服务保护我的 CDN (CloudFront) 并使用无服务器框架进行配置?
解决方案
这有点棘手,我需要大约一天的时间才能完成所有设置。
首先,我们在这里有选项:
- 我们可以签署 URL 并返回签名的 CloudFront URL 或签名的 S3 URL,而不是身份验证,这非常简单,但显然这不是我想要的。
- 第二个选项是使用 Lambda@Edge 来授权 CloudFront 的请求以及我遵循的请求。
所以我最终创建了一个单独的堆栈来处理所有 S3、CloudFront 和 Lambda@Edge 的东西,因为它们都部署在边缘上,这意味着区域无关紧要,但对于 lambda 边缘,我们需要将其部署到主 AWS region ((N. Virginia), us-east-1) 所以我最终为所有这些创建了一个堆栈。
首先,我的 auth-service.js 中有以下代码(这只是一些帮助我验证我的自定义 jwt):
import * as jwtDecode from 'jwt-decode';
import * as util from 'util';
import * as jwt from 'jsonwebtoken';
import * as jwksClient from 'jwks-rsa';
export function getToken(bearerToken) {
if(bearerToken && bearerToken.startsWith("Bearer "))
{
return bearerToken.replace(/^Bearer\s/, '');
}
throw new Error("Invalid Bearer Token.");
};
export function getDecodedHeader(token) {
return jwtDecode(token, { header: true });
};
export async function getSigningKey(decodedJwtTokenHeader, jwksclient){
const key = await util.promisify(jwksclient.getSigningKey)(decodedJwtTokenHeader.kid);
const signingKey = key.publicKey || key.rsaPublicKey;
if (!signingKey) {
throw new Error('could not get signing key');
}
return signingKey;
};
export async function verifyToken(token,signingKey){
return await jwt.verify(token, signingKey);
};
export function getJwksClient(jwksEndpoint){
return jwksClient({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 10,
jwksUri: jwksEndpoint
});
};
然后在 serverless.yml 里面是我的文件:
service: mda-app-uploads
plugins:
- serverless-offline
- serverless-pseudo-parameters
- serverless-iam-roles-per-function
- serverless-bundle
custom:
stage: ${opt:stage, self:provider.stage}
resourcesBucketName: ${self:custom.stage}-mda-resources-bucket
resourcesStages:
prod: prod
dev: dev
resourcesStage: ${self:custom.resourcesStages.${self:custom.stage}, self:custom.resourcesStages.dev}
provider:
name: aws
runtime: nodejs12.x
stage: ${opt:stage, 'dev'}
region: us-east-1
versionFunctions: true
functions:
oauthEdge:
handler: src/mda-edge-auth.handler
role: LambdaEdgeFunctionRole
memorySize: 128
timeout: 5
resources:
- ${file(resources/s3-cloudfront.yml)}
这里的快速点:
- us-east-1 在这里很重要。
- 使用无服务器框架创建任何 lambda 边缘有点棘手且不切实际,所以我用它来配置功能,然后在这个云形成模板
resources/s3-cloudfront.yml
中添加了所有需要的位。
那么这里的内容是resources/s3-cloudfront.yml
:
Resources:
AuthEdgeLambdaVersion:
Type: Custom::LatestLambdaVersion
Properties:
ServiceToken: !GetAtt PublishLambdaVersion.Arn
FunctionName: !Ref OauthEdgeLambdaFunction
Nonce: "Test"
PublishLambdaVersion:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Runtime: nodejs12.x
Role: !GetAtt PublishLambdaVersionRole.Arn
Code:
ZipFile: |
const {Lambda} = require('aws-sdk')
const {send, SUCCESS, FAILED} = require('cfn-response')
const lambda = new Lambda()
exports.handler = (event, context) => {
const {RequestType, ResourceProperties: {FunctionName}} = event
if (RequestType == 'Delete') return send(event, context, SUCCESS)
lambda.publishVersion({FunctionName}, (err, {FunctionArn}) => {
err
? send(event, context, FAILED, err)
: send(event, context, SUCCESS, {FunctionArn})
})
}
PublishLambdaVersionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: PublishVersion
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: lambda:PublishVersion
Resource: '*'
LambdaEdgeFunctionRole:
Type: "AWS::IAM::Role"
Properties:
Path: "/"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
-
Sid: "AllowLambdaServiceToAssumeRole"
Effect: "Allow"
Action:
- "sts:AssumeRole"
Principal:
Service:
- "lambda.amazonaws.com"
- "edgelambda.amazonaws.com"
LambdaEdgeFunctionPolicy:
Type: "AWS::IAM::Policy"
Properties:
PolicyName: MainEdgePolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
Effect: "Allow"
Action:
- "lambda:GetFunction"
- "lambda:GetFunctionConfiguration"
Resource: !GetAtt AuthEdgeLambdaVersion.FunctionArn
Roles:
- !Ref LambdaEdgeFunctionRole
ResourcesBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${self:custom.resourcesBucketName}
AccessControl: Private
CorsConfiguration:
CorsRules:
- AllowedHeaders: ['*']
AllowedMethods: ['PUT']
AllowedOrigins: ['*']
ResourcesBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket:
Ref: ResourcesBucket
PolicyDocument:
Statement:
# Read permission for CloudFront
- Action: s3:GetObject
Effect: "Allow"
Resource:
Fn::Join:
- ""
-
- "arn:aws:s3:::"
-
Ref: "ResourcesBucket"
- "/*"
Principal:
CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
- Action: s3:PutObject
Effect: "Allow"
Resource:
Fn::Join:
- ""
-
- "arn:aws:s3:::"
-
Ref: "ResourcesBucket"
- "/*"
Principal:
AWS: !GetAtt LambdaEdgeFunctionRole.Arn
- Action: s3:GetObject
Effect: "Allow"
Resource:
Fn::Join:
- ""
-
- "arn:aws:s3:::"
-
Ref: "ResourcesBucket"
- "/*"
Principal:
AWS: !GetAtt LambdaEdgeFunctionRole.Arn
CloudFrontOriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment:
Fn::Join:
- ""
-
- "Identity for accessing CloudFront from S3 within stack "
-
Ref: "AWS::StackName"
- ""
# Cloudfront distro backed by ResourcesBucket
ResourcesCdnDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
# S3 origin for private resources
- DomainName: !Sub '${self:custom.resourcesBucketName}.s3.amazonaws.com'
Id: S3OriginPrivate
S3OriginConfig:
OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/#{CloudFrontOriginAccessIdentity}'
# S3 origin for public resources
- DomainName: !Sub '${self:custom.resourcesBucketName}.s3.amazonaws.com'
Id: S3OriginPublic
S3OriginConfig:
OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/#{CloudFrontOriginAccessIdentity}'
Enabled: true
Comment: CDN for public and provate static content.
DefaultRootObject: index.html
HttpVersion: http2
DefaultCacheBehavior:
AllowedMethods:
- DELETE
- GET
- HEAD
- OPTIONS
- PATCH
- POST
- PUT
Compress: true
TargetOriginId: S3OriginPublic
ForwardedValues:
QueryString: false
Headers:
- Origin
Cookies:
Forward: none
ViewerProtocolPolicy: redirect-to-https
CacheBehaviors:
-
PathPattern: 'private/*'
TargetOriginId: S3OriginPrivate
AllowedMethods:
- DELETE
- GET
- HEAD
- OPTIONS
- PATCH
- POST
- PUT
Compress: true
LambdaFunctionAssociations:
-
EventType: viewer-request
LambdaFunctionARN: !GetAtt AuthEdgeLambdaVersion.FunctionArn
ForwardedValues:
QueryString: false
Headers:
- Origin
Cookies:
Forward: none
ViewerProtocolPolicy: redirect-to-https
-
PathPattern: 'public/*'
TargetOriginId: S3OriginPublic
AllowedMethods:
- DELETE
- GET
- HEAD
- OPTIONS
- PATCH
- POST
- PUT
Compress: true
ForwardedValues:
QueryString: false
Headers:
- Origin
Cookies:
Forward: none
ViewerProtocolPolicy: redirect-to-https
PriceClass: PriceClass_200
与此文件相关的一些要点:
- 在这里,我创建了包含所有私有和公共资源的 S3 存储桶。
- 此存储桶是私有的且不可访问,您将找到一个角色,该角色仅授予 CDN 和 lambda 边缘访问它的权限。
- 我决定创建一个 CloudFront (CDN),它有两个来源,公共指向 S3 的公用文件夹,私有指向 S3 的私有文件夹,并配置 CloudFront 私有来源的行为以使用我的 lambda 边缘功能进行身份验证查看者请求事件类型。
- 您还将找到一个代码来创建函数版本和另一个
PublishLambdaVersion
以其角色调用的函数,它有助于在部署时为 lambda 边缘提供正确的权限。
最后,这里是用于 CDN 身份验证的 lambda 边缘函数的实际代码:
import {getJwksClient, getToken, getDecodedHeader, getSigningKey, verifyToken} from '../../../../libs/services/auth-service';
import config from '../../../../config';
const response401 = {
status: '401',
statusDescription: 'Unauthorized'
};
exports.handler = async (event) => {
try{
const cfrequest = event.Records[0].cf.request;
const headers = cfrequest.headers;
if(!headers.authorization) {
console.log("no auth header");
return response401;
}
const jwtValue = getToken(headers.authorization);
const client = getJwksClient(`https://${config.authDomain}/.well-known/openid-configuration/jwks`);
const decodedJwtHeader = getDecodedHeader(jwtValue);
if(decodedJwtHeader)
{
const signingKey = await getSigningKey(decodedJwtHeader, client);
const verifiedToken = await verifyToken(jwtValue, signingKey);
if(verifiedToken)
{
return cfrequest;
}
}else{
throw Error("Unauthorized");
}
}catch(err){
console.log(err);
return response401;
}
};
如果您有兴趣,我正在使用 IdentityServer4 并将其作为 docker 映像托管在 Azure 中,并将其用作自定义授权方。
因此,现在我们拥有一个完全私有的 S3 存储桶,这就是完整的场景。它只能通过 CloudFront 源访问。如果请求通过公共源提供,则不需要身份验证,但如果它是通过私有源提供的,那么我将触发所谓的 lambda 边缘对其进行身份验证并验证承载令牌。
在深入了解所有这些之前,我对 AWS 堆栈完全陌生,但 AWS 非常简单,所以我最终以完美的方式配置了所有内容。如果有不清楚的地方或有任何问题,请告诉我。
推荐阅读
- javascript - vuejs输入未显示值
- c - 如何“单步执行”循环
- machine-learning - 如何将包含标记句子的文本输入神经网络?
- swift - 在swift中使用特殊字符分隔给定的字符串
- regex - 使用正则表达式的街道和数字格式:切换各种数字格式的顺序(例如 0-9 或 0-9a-z 或 0-9-0-9a-z),然后是文本
- c++ - 类函数不会永久更改成员变量
- javascript - 石头剪刀布游戏(js) - 如何创建一个圆形功能
- html - 提供用户选项以通过按钮将评估的 Google 脚本 html 模板下载为 pdf
- android - 问题:发现现有项目依赖项中的不一致。之间的版本不兼容: - com.google.firebase:firebase-auth:19.0.0@aar
- python - 我想在 python 中制作 3*3 矩阵格式列表