如何通过CDK为S3托管的静态React应用配置专属上传权限?
静态React应用通过CloudFront上传文件到S3的权限配置方案
针对你用CDK部署的S3静态站点,要实现仅允许你的应用用户上传文件的需求,核心问题在于静态前端无法安全持有AWS IAM凭证(硬编码会直接泄露),所以必须通过「安全的凭证分发+权限限制」的方式实现。以下是几种生产环境常用的方案,结合CDK代码实现:
方案1:Cognito认证+S3预签名URL(推荐)
通过Cognito对用户进行身份认证,获取临时IAM凭证后生成S3预签名URL,前端用这个URL完成上传。这种方式既能控制用户权限,又能避免凭证泄露。
CDK代码实现(Kotlin)
// 创建Cognito用户池,用于用户注册/登录 val userPool = UserPool(this, "AppUserPool", UserPoolProps.builder() .selfSignUpEnabled(true) .accountRecovery(UserPoolAccountRecovery.EMAIL_ONLY) .userVerification(UserPoolUserVerificationConfig.builder() .emailSubject("验证你的应用账号") .emailBody("点击链接完成验证:{##Verify Email##}") .emailStyle(UserPoolEmailStyle.LINK) .build()) .build()) // 创建用户池客户端,供前端调用 val userPoolClient = userPool.addClient("AppUserPoolClient", UserPoolClientProps.builder() .authFlows(AuthFlow.builder() .userPassword(true) .userSrp(true) .build()) .build()) // 创建身份池,关联用户池,用于分发临时IAM凭证 val identityPool = CfnIdentityPool(this, "AppIdentityPool", CfnIdentityPoolProps.builder() .allowUnauthenticatedIdentities(false) // 若允许匿名上传可设为true,但不推荐 .userPoolId(userPool.userPoolId) .userPoolClientId(userPoolClient.userPoolClientId) .build()) // 为认证用户创建IAM角色,赋予S3上传权限 val authenticatedRole = Role(this, "AuthenticatedRole", RoleProps.builder() .assumedBy(FederatedPrincipal("cognito-identity.amazonaws.com", mapOf( "StringEquals" to mapOf("cognito-identity.amazonaws.com:aud" to identityPool.ref), "ForAnyValue:StringLike" to mapOf("cognito-identity.amazonaws.com:amr" to "authenticated") ), "sts:AssumeRoleWithWebIdentity")) .build()) // 限制权限:仅允许上传到uploads目录,且文件设为私有 authenticatedRole.addToPolicy(PolicyStatement.Builder.create() .actions(listOf("s3:PutObject")) .resources(listOf("${siteBucket.bucketArn}/uploads/*")) .conditions(mapOf( "StringEquals" to mapOf("s3:x-amz-acl" to "private") )) .build()) // 输出身份池ID,供前端配置使用 CfnOutput(this, "IdentityPoolId", CfnOutputProps.builder() .value(identityPool.ref) .build())
前端实现思路
用AWS Amplify或AWS SDK v3,先通过Cognito完成用户登录,获取临时凭证后调用S3的getSignedUrlPromise生成预签名URL,最后用这个URL发起PUT请求上传文件。
方案2:Lambda@Edge生成预签名URL(无需用户认证)
如果不需要用户注册登录,可通过CloudFront的Lambda@Edge函数在边缘节点生成预签名URL,前端先请求这个接口拿到URL再上传。同时可验证请求来源是否为你的CloudFront域名,拦截非法请求。
CDK代码实现(Kotlin)
// 创建生成预签名URL的Lambda函数 val generatePresignedUrlFunction = Function(this, "GeneratePresignedUrlFunction", FunctionProps.builder() .runtime(Runtime.NODEJS_18_X) .handler("index.handler") .code(Code.fromAsset("lambda/generate-presigned-url")) // 指向本地Lambda代码目录 .environment(mapOf("S3_BUCKET_NAME" to siteBucket.bucketName)) .build()) // 赋予Lambda函数S3上传权限 siteBucket.grantPut(generatePresignedUrlFunction) // 更新CloudFront配置,添加专属行为触发Lambda val distribution = CloudFrontWebDistribution(this, "SiteDistribution", CloudFrontWebDistributionProps.builder() .originConfigs(listOf(SourceConfiguration.builder() .s3OriginSource(S3OriginConfig.builder() .s3BucketSource(siteBucket) .build()) .behaviors(listOf(Behavior.builder() .isDefaultBehavior(true) .build())) .build())) .additionalBehaviors(mapOf( "/get-upload-url/*" to Behavior.builder() .lambdaFunctionAssociations(listOf(LambdaFunctionAssociation.builder() .eventType(LambdaEdgeEventType.VIEWER_REQUEST) .lambdaFunction(generatePresignedUrlFunction.currentVersion) .build())) .build() )) .build()) // 允许CloudFront调用Lambda函数 generatePresignedUrlFunction.addPermission("AllowCloudFrontInvocation", PermissionProps.builder() .principal(ServicePrincipal("cloudfront.amazonaws.com")) .action("lambda:InvokeFunction") .sourceArn("arn:aws:cloudfront::${Account.of(this)}:distribution/${distribution.distributionId}") .build())
Lambda函数代码(Node.js)
const AWS = require('aws-sdk'); const s3 = new AWS.S3(); exports.handler = async (event) => { const request = event.Records[0].cf.request; // 验证请求来源是否为你的CloudFront域名 const allowedHost = "your-app-domain.com"; if (request.headers.host[0].value !== allowedHost) { return { status: '403', statusDescription: 'Forbidden' }; } // 提取文件名,生成5分钟有效期的预签名URL const fileName = decodeURIComponent(request.uri.split('/').pop()); const params = { Bucket: process.env.S3_BUCKET_NAME, Key: `uploads/${fileName}`, Expires: 300 }; try { const presignedUrl = await s3.getSignedUrlPromise('putObject', params); return { status: '200', statusDescription: 'OK', body: JSON.stringify({ uploadUrl: presignedUrl }), headers: { 'content-type': [{ key: 'Content-Type', value: 'application/json' }] } }; } catch (err) { return { status: '500', statusDescription: 'Internal Server Error' }; } };
关键注意事项
- 绝对禁止硬编码IAM凭证:前端代码会被用户下载,硬编码凭证会导致任何人都能操作你的S3存储桶。
- 权限最小化:无论是IAM角色还是Lambda权限,都要限制到特定的S3前缀(如
uploads/*),避免全桶权限。 - 文件管控:可在IAM策略或Lambda中添加条件,限制上传文件的大小、类型(如通过
s3:content-length-range条件)。 - 上传文件权限:默认设置为私有,若需要公开访问,可通过CloudFront配置OAC(Origin Access Control)指向S3,避免直接公开存储桶。
内容的提问来源于stack exchange,提问作者AlexJ
相关产品推荐
相关产品推荐

