如何用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
相关产品推荐
相关产品推荐

