上下文

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.S3Originconstruct 创建了所谓的Origin Access Identity (OAI)。每当 Amazon CloudFront 与给定源(在我们的例子中为assets-bucket)通信时,都会使用此身份。

使用 OAI 后,Amazon CloudFront 将为给定源的每个请求添加一个Authorization标头。对于我们来说,这种行为会在预签名 URL 的查询参数中包含的授权相关信息与Authorization标头之间产生冲突。

由于S3Originconstruct,据我所知,不允许我们配置是否要创建 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

  • 本文使用的代码

谢谢👋

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐