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

如何通过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' };
  }
};

关键注意事项

  1. 绝对禁止硬编码IAM凭证:前端代码会被用户下载,硬编码凭证会导致任何人都能操作你的S3存储桶。
  2. 权限最小化:无论是IAM角色还是Lambda权限,都要限制到特定的S3前缀(如uploads/*),避免全桶权限。
  3. 文件管控:可在IAM策略或Lambda中添加条件,限制上传文件的大小、类型(如通过s3:content-length-range条件)。
  4. 上传文件权限:默认设置为私有,若需要公开访问,可通过CloudFront配置OAC(Origin Access Control)指向S3,避免直接公开存储桶。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 02:10:51