如何配置AWS S3+CloudFront实现JWT授权的私有文件访问(C#)
解决方案:CloudFront + 私有S3桶的JWT授权访问方案
一、核心架构思路
用CloudFront作为图片访问的唯一入口,S3保持完全私有。通过CloudFront的CloudFront Functions(轻量低成本)或Lambda@Edge(复杂逻辑场景)在请求阶段验证JWT令牌:
- 有效令牌:允许请求穿透到S3返回图片
- 无效/无令牌:返回自定义未授权XML错误(匹配你提供的示例格式)
二、AWS服务配置步骤
1. S3私有桶配置(保持原有私有设置)
- 确认存储桶已启用「阻止所有公共访问」,无需修改
- 创建Origin Access Control (OAC):
- 进入CloudFront控制台,新建OAC,类型选「S3」,勾选「签名请求」
- 关联到你的S3桶,更新桶策略,仅允许该OAC对应的CloudFront分发访问:
{ "Version": "2008-10-17", "Id": "PolicyForCloudFrontPrivateContent", "Statement": [ { "Sid": "AllowCloudFrontAccess", "Effect": "Allow", "Principal": { "Service": "cloudfront.amazonaws.com" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*", "Condition": { "StringEquals": { "AWS:SourceArn": "arn:aws:cloudfront::YOUR_AWS_ACCOUNT_ID:distribution/YOUR_CLOUDFRONT_DIST_ID" } } } ] }
- 上传图片到S3,保持对象默认私有权限
2. CloudFront分发配置
- 新建分发,源选择你的S3桶,关联上述OAC
- 行为设置:
- 路径模式设为
/* - 缓存策略选「Managed-CachingDisabled」(避免缓存未授权请求)
- 绑定JWT验证逻辑到「Viewer Request」阶段
- 路径模式设为
3. JWT验证逻辑配置
场景1:轻量验证用CloudFront Functions
创建CloudFront Function,代码如下(直接匹配你要的错误格式):
function handler(event) { const request = event.request; const authHeader = request.headers.authorization; // 从Authorization头提取Bearer令牌(localStorage的令牌需前端通过该头携带) if (!authHeader || !authHeader.value.startsWith('Bearer ')) { return generateUnauthorizedResponse(); } const token = authHeader.value.split(' ')[1]; try { // 解析JWT payload(生产环境需验证签名,这里示例仅检查过期时间) const payload = JSON.parse(atob(token.split('.')[1])); if (payload.exp * 1000 < Date.now()) { return generateUnauthorizedResponse(); } // 可额外验证受众、发行方等字段 if (payload.aud !== "YOUR_APP_AUDIENCE") { return generateUnauthorizedResponse(); } } catch (e) { return generateUnauthorizedResponse(); } return request; } function generateUnauthorizedResponse() { const requestId = Math.random().toString(36).substring(2, 18); return { statusCode: 403, statusDescription: 'Forbidden', headers: { 'content-type': { value: 'application/xml' } }, body: { encoding: 'text', data: `<?xml version="1.0" encoding="UTF-8"?> <Error> <Code>ResourceNotFound</Code> <Message>The specified resource does not exist. RequestId:${requestId} Time:${new Date().toISOString()}</Message> </Error>` } }; }
将该函数关联到CloudFront分发的「Viewer Request」阶段。
场景2:复杂验证用Lambda@Edge
如果需要调用外部授权服务(如Cognito、Auth0)验证JWT,用Lambda@Edge(Node.js示例):
const jwt = require('jsonwebtoken'); const PUBLIC_KEY = 'YOUR_JWT_PUBLIC_KEY'; exports.handler = async (event) => { const request = event.Records[0].cf.request; const authHeader = request.headers.authorization; if (!authHeader || !authHeader[0].value.startsWith('Bearer ')) { return generateUnauthorizedResponse(); } const token = authHeader[0].value.split(' ')[1]; try { jwt.verify(token, PUBLIC_KEY, { audience: "YOUR_APP_AUDIENCE" }); } catch (err) { return generateUnauthorizedResponse(); } return request; }; function generateUnauthorizedResponse() { const requestId = Math.random().toString(36).substring(2, 18); return { status: '403', statusDescription: 'Forbidden', headers: { 'content-type': [{ key: 'Content-Type', value: 'application/xml' }] }, body: `<?xml version="1.0" encoding="UTF-8"?> <Error> <Code>ResourceNotFound</Code> <Message>The specified resource does not exist. RequestId:${requestId} Time:${new Date().toISOString()}</Message> </Error>` }; }
4. 自定义错误页面(可选)
在CloudFront分发的「错误页面」设置中,添加403错误规则:
- HTTP错误代码:403
- 自定义响应代码:403
- 直接用上述函数返回的XML内容,无需额外指定页面路径
三、C#实现代码
1. 生成JWT令牌(后端)
使用System.IdentityModel.Tokens.Jwt库生成合规令牌:
using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Microsoft.IdentityModel.Tokens; public string GenerateValidJwt(string userId) { var secretKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("YOUR_JWT_SECRET_KEY")); var signingCreds = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId), new Claim(JwtRegisteredClaimNames.Aud, "YOUR_APP_AUDIENCE"), new Claim(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeSeconds().ToString()) }; var token = new JwtSecurityToken( issuer: "YOUR_APP_ISSUER", audience: "YOUR_APP_AUDIENCE", claims: claims, expires: DateTime.UtcNow.AddHours(1), signingCredentials: signingCreds); return new JwtSecurityTokenHandler().WriteToken(token); }
2. 上传图片到S3(后端)
使用AWS SDK for .NET上传私有对象:
using Amazon.S3; using Amazon.S3.Transfer; public async Task UploadImageToPrivateBucket(string bucketName, string localFilePath, string s3ObjectKey) { // 优先用IAM角色或环境变量配置凭证,避免硬编码 var s3Client = new AmazonS3Client(); var transferUtility = new TransferUtility(s3Client); await transferUtility.UploadAsync(localFilePath, bucketName, s3ObjectKey); }
3. 前端携带令牌请求图片
前端从localStorage/Cookie取令牌,通过Authorization头发送请求:
// 配合C#后端生成的令牌 const jwtToken = localStorage.getItem("userJwt"); const imageUrl = "https://YOUR_CLOUDFRONT_DOMAIN/images/photo.jpg"; fetch(imageUrl, { headers: { "Authorization": `Bearer ${jwtToken}` } }) .then(res => res.blob()) .then(blob => { const img = document.createElement("img"); img.src = URL.createObjectURL(blob); document.body.appendChild(img); }) .catch(err => console.log("未授权访问:", err));
四、测试验证
- 用有效JWT请求CloudFront图片URL,应正常返回图片
- 不携带令牌/用过期令牌请求,应返回你指定格式的XML错误
内容的提问来源于stack exchange,提问作者Ming Hieu
相关产品推荐
相关产品推荐

