个使用 Amazon CloudFront 和 Amazon S3 的一次性预签名 URL
上下文
Amazon S3 是一种对象存储服务,与许多其他功能一起,您可以创建预签名 URL,使外部用户能够访问给定 S3 存储桶中的对象。
虽然可以设置 URL 的到期日期,但没有使其成为一次性使用资源的本机内置功能。
以下是如何使用 AWS 公开的构建块添加此类功能的示例。
高层架构
[](https://res.cloudinary.com/practicaldev/image/fetch/s--aV9ZPBm_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads .s3.amazonaws.com/uploads/articles/92aw2dwqmnglm6mesdht.jpeg)
实施
我将使用 AWS CDK 作为我选择的 IAC 工具,并使用 TypeScript 作为实施的编程语言。
首先,AWS DynamoDB 表用于存储有关给定预签名 URL 的使用情况的信息,以及用于保存对象的 Amazon S3 存储桶。
import * as dynamo from "@aws-cdk/aws-dynamodb";
import * as s3 from "@aws-cdk/aws-s3";
const entriesTable = new dynamo.Table(this, "entriesTable", {
partitionKey: { name: "pk", type: dynamo.AttributeType.STRING },
billingMode: dynamo.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY
});
const bucket = new s3.Bucket(this, "assets-bucket", {
removalPolicy: cdk.RemovalPolicy.DESTROY
});
进入全屏模式 退出全屏模式
接下来,_AWS Lambda 前面是 Amazon API Gateway。这部分基础架构负责生成预签名 URL。
import * as lambda from "@aws-cdk/aws-lambda-nodejs";
import * as apigw from "@aws-cdk/aws-apigatewayv2";
import * as apigwIntegrations from "@aws-cdk/aws-apigatewayv2-integrations";
const urlLambda = new lambda.NodejsFunction(this, "urlLambda", {
entry: join(__dirname, "./url-lambda.ts"),
environment: {
BUCKET_NAME: bucket.bucketName
}
});
bucket.grantRead(urlLambda);
const api = new apigw.HttpApi(this, "api", {
corsPreflight: {
allowMethods: [apigw.CorsHttpMethod.GET]
}
});
api.addRoutes({
integration: new apigwIntegrations.LambdaProxyIntegration({
handler: urlLambda
}),
path: "/get-url",
methods: [apigw.HttpMethod.GET]
});
进入全屏模式 退出全屏模式
对于urlLambda
(实现参考) 返回具有正确域的预签名 URL 至关重要。
默认情况下,如果您使用 Amazon S3 开发工具包,则预签名 URL 包含 Amazon S3 域。在我们的例子中,必须将域交换为 Amazon CloudFront 公开的域。这将允许我们在请求 URL 时运行代码 (Lambda@Edge)。
对于最后一部分,带有 Lambda@Edge(实施参考)的 Amazon CloudFront 分配将记录预签名 URL 的使用情况并决定是否应允许请求。
/**
* Lambda@Edge does not support environment variables.
* To forward the `entriesTable` name generated by CloudFormation,
* an SSM parameter is created.
*
* This parameter will be fetched during the runtime of the `edgeLambda`.
*/
const entriesTableParameter = new ssm.StringParameter(
this,
"URL_ENTRIES_TABLE_NAME",
{
stringValue: entriesTable.tableName,
parameterName: "URL_ENTRIES_TABLE_NAME"
}
);
const edgeLambda = new lambda.NodejsFunction(this, "edgeLambda", {
entry: join(__dirname, "./edge-lambda.ts")
});
entriesTable.grantReadWriteData(edgeLambda.currentVersion);
entriesTableParameter.grantRead(edgeLambda.currentVersion);
const distribution = new cloudfront.Distribution(this, "distribution", {
defaultBehavior: {
origin: new origins.S3Origin(bucket),
cachePolicy: new cloudfront.CachePolicy(this, "cachePolicy", {
maxTtl: cdk.Duration.seconds(1),
minTtl: cdk.Duration.seconds(0),
defaultTtl: cdk.Duration.seconds(0),
// QueryStrings from presigned URL have to be forwarded to S3.
queryStringBehavior: cloudfront.CacheQueryStringBehavior.all()
}),
edgeLambdas: [
{
eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
functionVersion: edgeLambda.currentVersion,
includeBody: false
}
]
}
});
urlLambda.addEnvironment("CF_DOMAIN", distribution.domainName);
进入全屏模式 退出全屏模式
用途
下面示例命令中使用的所有变量都可用作堆栈输出。
部署完成后,让我们上传一个对象到assets-bucket
。我要上传一个猫的图像。
aws s3 mv cat.jpeg s3://<assetsBucketName>
进入全屏模式 退出全屏模式
上传对象后,我们可以请求预签名 URL。
curl -XGET 'https://<getPresignedUrlEndpoint>?key=cat.jpeg'
进入全屏模式 退出全屏模式
现在我们已经准备好测试我们的解决方案了。理论上,从上一个命令返回的对 URL 的第一个 GET 请求应该会成功。所有后续请求都应失败并显示 403 状态代码。
可悲的是,当我们发出请求时,将返回一个错误
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>InvalidArgument</Code><Message>Only one auth mechanism allowed; only the X-Amz-Algorithm query parameter, Signature query string parameter or the Authorization header should be specified</Message>
<ArgumentName>Authorization</ArgumentName><ArgumentValue>AWS4-HMAC-SHA256 Credential=AKIAIJPQUQ6PR4TR73SQ/20210516/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=f87f653cce70a3dd3260c1e5d187bc62aa8768ed606f59b29d811febe6880b20</ArgumentValue>
<RequestId>SBKQ3G6QDEJSJTRX</RequestId><HostId>8oCArh0jBSNahbv5A8keMVuNk2HFDF5ud52YHurBB7tMCxnEhNmqZU/4wJiaQ7WR70NhJLfH934=</HostId></Error>
进入全屏模式 退出全屏模式
修复认证机制
由于代码的 IaC 部分中使用的一些 AWS CDK 构造 的高级特性,错误消息可能根本没有帮助。我们没有在代码中的任何地方明确设置Authorization
标头,所以发生了什么?
事实证明,origins.S3Origin
construct 创建了所谓的Origin Access Identity (OAI)。每当 Amazon CloudFront 与给定源(在我们的例子中为assets-bucket
)通信时,都会使用此身份。
使用 OAI 后,Amazon CloudFront 将为给定源的每个请求添加一个Authorization
标头。对于我们来说,这种行为会在预签名 URL 的查询参数中包含的授权相关信息与Authorization
标头之间产生冲突。
由于S3Origin
construct,据我所知,不允许我们配置是否要创建 OAI,我们可以利用 AWS CDK 逃生舱口之一直接修改底层 CloudFormation 模板 - 有效地删除创建的 OAI。
const distribution = new cloudfront.Distribution(this, "distribution", {}); // Defined previously.
const cfnDistribution = distribution.node
.defaultChild as cloudfront.CfnDistribution;
cfnDistribution.addPropertyOverride(
"DistributionConfig.Origins.0.S3OriginConfig.OriginAccessIdentity",
""
);
进入全屏模式 退出全屏模式
删除assets-bucket
源的 OAI 并重新部署堆栈后,之前的 GET 请求应该按预期工作。
如果您对
node.defaultChild
的工作原理感到好奇,这是一个很棒的视频,您可以观看。
总结
我创建此架构是为了提高我对_Amazon CloudFormation_ 的了解。底层实现被简化为易于理解。
请不要将其视为实现基本目标的唯一可能方法,肯定有更便宜的方法可以做到这一点! (与常规 AWS Lambda 相比,Lambda@Edge 成本相对较高)。
-
我在推特上 -@wm \ _matuszewski
-
本文使用的代码
谢谢👋
更多推荐
所有评论(0)