You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用CDK实现Presigned URL单次上传限制?求方案2/3代码示例

实现一次性有效S3预签名URL(CDK方案)

以下针对你提到的方案2和方案3,结合CDK代码给出具体实现:


方案2:通过Lambda失效IAM访问密钥实现一次性上传

核心逻辑

创建临时IAM用户并生成专属访问密钥,用该密钥生成预签名URL;文件上传完成后,触发Lambda删除该用户的访问密钥(或直接删除用户),让预签名URL立即失效。

CDK代码实现(TypeScript)

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as s3n from 'aws-cdk-lib/aws-s3-notifications';

export class OneTimePresignedUrlStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 1. 创建用于接收上传的S3桶
    const uploadBucket = new s3.Bucket(this, 'OneTimeUploadBucket', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    // 2. 上传完成后触发的Lambda:失效临时用户的访问密钥
    const invalidateKeyLambda = new lambda.Function(this, 'InvalidateKeyLambda', {
      runtime: lambda.Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: lambda.Code.fromInline(`
        const AWS = require('aws-sdk');
        const iam = new AWS.IAM();

        exports.handler = async (event) => {
          // 从S3事件中提取临时用户ID(生成URL时约定路径格式为 temp-user-xxx/文件名)
          const objectKey = event.Records[0].s3.object.key;
          const tempUserId = objectKey.split('/')[0];

          // 删除临时用户的所有访问密钥
          const accessKeys = await iam.listAccessKeys({ UserName: tempUserId }).promise();
          for (const key of accessKeys.AccessKeyMetadata) {
            await iam.deleteAccessKey({ UserName: tempUserId, AccessKeyId: key.AccessKeyId }).promise();
          }

          // 可选:直接删除临时用户,彻底清理资源
          await iam.deleteUser({ UserName: tempUserId }).promise();

          return { statusCode: 200, body: '密钥已失效' };
        };
      `),
      role: new iam.Role(this, 'InvalidateKeyLambdaRole', {
        assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
        managedPolicies: [
          // 生产环境请缩小权限,仅授予IAM用户/密钥的删除权限
          iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonIAMFullAccess'),
          iam.ManagedPolicy.fromAwsManagedPolicyName('AWSLambdaBasicExecutionRole'),
        ],
      }),
    });

    // 配置S3桶的PutObject事件触发Lambda
    uploadBucket.addEventNotification(s3.EventType.OBJECT_CREATED_PUT, new s3n.LambdaDestination(invalidateKeyLambda));

    // 3. 生成预签名URL的Lambda:创建临时用户并生成URL
    const generatePresignedUrlLambda = new lambda.Function(this, 'GeneratePresignedUrlLambda', {
      runtime: lambda.Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: lambda.Code.fromInline(`
        const AWS = require('aws-sdk');
        const s3 = new AWS.S3();
        const iam = new AWS.IAM();

        exports.handler = async (event) => {
          // 创建唯一的临时IAM用户
          const tempUserId = \`temp-user-\${Date.now()}\`;
          await iam.createUser({ UserName: tempUserId }).promise();

          // 给临时用户赋予上传指定路径的权限
          const uploadPolicy = {
            Version: '2012-10-17',
            Statement: [{
              Effect: 'Allow',
              Action: 's3:PutObject',
              Resource: \`${uploadBucket.bucketArn}/\${tempUserId}/*\`
            }]
          };
          await iam.putUserPolicy({
            UserName: tempUserId,
            PolicyName: 'TempUploadPolicy',
            PolicyDocument: JSON.stringify(uploadPolicy)
          }).promise();

          // 生成临时访问密钥
          const accessKey = await iam.createAccessKey({ UserName: tempUserId }).promise();
          const tempCredentials = {
            accessKeyId: accessKey.AccessKey.AccessKeyId,
            secretAccessKey: accessKey.AccessKey.SecretAccessKey
          };

          // 使用临时密钥生成预签名URL
          const s3Client = new AWS.S3(tempCredentials);
          const presignedUrl = s3Client.getSignedUrl('putObject', {
            Bucket: '${uploadBucket.bucketName}',
            Key: \`\${tempUserId}/\${event.queryStringParameters.filename}\`,
            Expires: 300 // 基础过期时间,配合密钥失效实现一次性限制
          });

          return {
            statusCode: 200,
            body: JSON.stringify({ presignedUrl, uploadPath: \`\${tempUserId}/\${event.queryStringParameters.filename}\` })
          };
        };
      `),
      role: new iam.Role(this, 'GeneratePresignedUrlLambdaRole', {
        assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
        managedPolicies: [
          // 生产环境请缩小权限,仅授予IAM用户/密钥的创建权限
          iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonIAMFullAccess'),
          iam.ManagedPolicy.fromAwsManagedPolicyName('AWSLambdaBasicExecutionRole'),
        ],
      }),
    });

    // 输出生成URL的Lambda ARN,方便后续对接API Gateway或其他触发方式
    new cdk.CfnOutput(this, 'GeneratePresignedUrlLambdaArn', {
      value: generatePresignedUrlLambda.functionArn,
    });
  }
}

注意事项

  • 生产环境必须严格缩小IAM权限,避免使用AmazonIAMFullAccess,仅授予必要的创建/删除用户、密钥、策略的权限。
  • 临时用户的命名规则可根据业务调整,确保能从上传路径中关联到对应的用户。

方案3:使用Parameter Store存储临时凭证实现一次性上传

核心逻辑

生成临时STS凭证并存储到SSM Parameter Store,标记为有效状态;生成预签名URL时使用该凭证;文件上传完成后,触发Lambda修改Parameter Store中的状态为无效,同时STS凭证自身的过期时间作为双重保障。

CDK代码实现(TypeScript)

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as s3n from 'aws-cdk-lib/aws-s3-notifications';
import * as ssm from 'aws-cdk-lib/aws-ssm';

export class OneTimePresignedUrlStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 1. 创建用于接收上传的S3桶
    const uploadBucket = new s3.Bucket(this, 'OneTimeUploadBucket', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    // 2. 创建SSM Parameter Store参数,存储临时凭证及有效状态
    const tempCredsParam = new ssm.StringParameter(this, 'TempUploadCredsParam', {
      parameterName: '/one-time-upload/temp-creds',
      stringValue: JSON.stringify({ valid: false, accessKeyId: '', secretAccessKey: '', sessionToken: '' }),
      type: ssm.ParameterType.STRING,
    });

    // 3. 上传完成后触发的Lambda:将凭证状态设为无效
    const invalidateCredsLambda = new lambda.Function(this, 'InvalidateCredsLambda', {
      runtime: lambda.Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: lambda.Code.fromInline(`
        const AWS = require('aws-sdk');
        const ssm = new AWS.SSM();

        exports.handler = async (event) => {
          // 更新Parameter Store,标记凭证无效
          await ssm.putParameter({
            Name: '${tempCredsParam.parameterName}',
            Value: JSON.stringify({ valid: false, accessKeyId: '', secretAccessKey: '', sessionToken: '' }),
            Overwrite: true,
            Type: 'String'
          }).promise();

          return { statusCode: 200, body: '凭证已失效' };
        };
      `),
      role: new iam.Role(this, 'InvalidateCredsLambdaRole', {
        assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
        managedPolicies: [
          // 生产环境请缩小权限,仅授予SSM参数的修改权限
          iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMFullAccess'),
          iam.ManagedPolicy.fromAwsManagedPolicyName('AWSLambdaBasicExecutionRole'),
        ],
      }),
    });

    // 配置S3桶的PutObject事件触发Lambda
    uploadBucket.addEventNotification(s3.EventType.OBJECT_CREATED_PUT, new s3n.LambdaDestination(invalidateCredsLambda));

    // 4. 生成预签名URL的Lambda:生成STS凭证并更新Parameter Store
    const generatePresignedUrlLambda = new lambda.Function(this, 'GeneratePresignedUrlLambda', {
      runtime: lambda.Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: lambda.Code.fromInline(`
        const AWS = require('aws-sdk');
        const sts = new AWS.STS();
        const ssm = new AWS.SSM();

        exports.handler = async (event) => {
          // 生成临时STS凭证,有效期15分钟
          const stsResponse = await sts.getSessionToken({
            DurationSeconds: 900,
          }).promise();
          const tempCredentials = {
            accessKeyId: stsResponse.Credentials.AccessKeyId,
            secretAccessKey: stsResponse.Credentials.SecretAccessKey,
            sessionToken: stsResponse.Credentials.SessionToken
          };

          // 更新Parameter Store,标记凭证有效
          await ssm.putParameter({
            Name: '${tempCredsParam.parameterName}',
            Value: JSON.stringify({ ...tempCredentials, valid: true }),
            Overwrite: true,
            Type: 'String'
          }).promise();

          // 使用临时STS凭证生成预签名URL
          const s3Client = new AWS.S3(tempCredentials);
          const presignedUrl = s3Client.getSignedUrl('putObject', {
            Bucket: '${uploadBucket.bucketName}',
            Key: event.queryStringParameters.filename,
            Expires: 300
          });

          return {
            statusCode: 200,
            body: JSON.stringify({ presignedUrl })
          };
        };
      `),
      role: new iam.Role(this, 'GeneratePresignedUrlLambdaRole', {
        assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
        managedPolicies: [
          // 生产环境请缩小权限,仅授予STS凭证生成和SSM参数修改权限
          iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSTSFullAccess'),
          iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMFullAccess'),
          iam.ManagedPolicy.fromAwsManagedPolicyName('AWSLambdaBasicExecutionRole'),
        ],
      }),
    });

    // 输出生成URL的Lambda ARN
    new cdk.CfnOutput(this, 'GeneratePresignedUrlLambdaArn', {
      value: generatePresignedUrlLambda.functionArn,
    });
  }
}

注意事项

  • 可配合S3桶策略,添加基于Parameter Store的条件判断,进一步确保只有凭证有效时才能完成上传。
  • STS凭证的过期时间可根据业务需求调整,避免无效凭证长时间存在。

内容的提问来源于stack exchange,提问作者DarkCrow

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 03:50:54