![]()
去年有个做电商的朋友跟我吐槽:用户上传的头像存在MySQL里,数据库半年涨了800G,备份一次要通宵。我让他把图片迁到S3,只存引用,DB瞬间瘦回30G——这就是对象存储和关系型数据库的分工逻辑。
但迁上去只是第一步。真正让后端头疼的是:怎么防盗链?怎么让前端安全读取?怎么不让桶变成公共图床?
这篇指南从Node.js上传逻辑到IAM策略、CORS锁死,把每一层都拆开讲。适合已经会用AWS SDK、但还没搞懂权限体系怎么搭的开发者。
第一步:桶的创建和"假公开"陷阱
很多人创建S3桶时顺手点了"允许公共访问",或者以为Block Public Access全开就万事大吉——这两种都错。
正确的姿势是:用CLI创建时显式指定区域,然后关闭ACL级别的公共访问,但保留桶策略的口子。
代码如下:
aws s3api create-bucket \
--bucket your-app-images \
--region ap-south-1 \
--create-bucket-configuration LocationConstraint=ap-south-1
aws s3api put-public-access-block \
--bucket your-app-images \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=false,RestrictPublicBuckets=false"
注意最后两个参数是false。我们要的不是"完全封闭",而是"只认规则的封闭"——接下来用桶策略精确控制谁可以读。
第二步:桶策略的"Referer白名单"机制
S3的权限体系有两层:IAM(谁可以操作桶)和桶策略(什么条件下允许访问)。这里我们专注后者。
核心思路是利用HTTP的Referer头。当浏览器从你的网站加载图片时,会自动带上你的域名;直接复制URL到地址栏,或者别的网站嵌套你的图,Referer就不匹配。
策略文件长这样:
"Version": "2012-10-17",
"Statement": [
"Sid": "AllowOnlyFromMyWebsite",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-app-images/public/*",
"Condition": {
"StringLike": {
"aws:Referer": [
"https://yourwebsite.com/*",
"https://www.yourwebsite.com/*"
应用策略:
aws s3api put-bucket-policy \
--bucket your-app-images \
--policy file://s3-bucket-policy.json
这个方案只适用于"公开但防 hotlink"的场景,比如商品图、文章配图。如果是用户私有文件(身份证、合同),别用Referer策略,直接走预签名URL——下面会讲。
第三步:CORS配置和前端的"跨域焦虑"
桶策略管的是"能不能读",CORS管的是"浏览器让不让读"。如果你的前端用fetch或XMLHttpRequest直接请求S3,没配CORS会报经典错误:
Access to fetch at 'https://your-bucket.s3...' from origin 'https://yourwebsite.com' has been blocked by CORS policy.
配置CORS允许你的域名:
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedOrigins": ["https://yourwebsite.com"],
"ExposeHeaders": [],
"MaxAgeSeconds": 3000
这里有个细节:AllowedOrigins不要写*,哪怕你暂时只有一个域名。未来加子域名或换主域时,你会感谢自己当初没偷懒。
第四步:Node.js上传逻辑——流式处理+元数据
前端把文件传给Node.js API,API再上传到S3。这个设计有两个好处:一是可以预处理(压缩、格式校验、病毒扫描),二是隐藏真实的S3桶名和路径结构。
用@aws-sdk/client-s3和@aws-sdk/lib-storage实现流式上传:
import { S3Client } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
const s3Client = new S3Client({ region: 'ap-south-1' });
async function uploadImage(fileBuffer, fileName, mimeType) {
const s3Key = `public/${Date.now()}-${fileName}`;
const upload = new Upload({
client: s3Client,
params: {
Bucket: 'your-app-images',
Key: s3Key,
Body: fileBuffer,
ContentType: mimeType,
Metadata: {
'uploaded-by': 'user-id-123',
'original-name': fileName
const result = await upload.done();
return s3Key; // 只返回key,不返回URL
关键细节:Metadata字段可以存业务信息,比如上传者ID、原始文件名,方便后续审计和迁移。但别存敏感信息,S3元数据是明文的。
数据库只存s3Key,查询时动态生成URL。这样即使未来迁移到别的存储(Cloudflare R2、MinIO),只需改URL生成逻辑,不用改数据库。
第五步:预签名URL——私有文件的"临时通行证"
对于用户私有文件,桶策略那套Referer机制不够用:一旦URL泄露,任何人都能看。这时候需要预签名URL(Pre-signed URL)。
原理:用你账号的IAM凭证对请求进行签名,生成一个带过期时间的临时链接。S3验证签名有效且未过期,才返回文件。
实现:
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { GetObjectCommand } from '@aws-sdk/client-s3';
async function getPrivateImageUrl(s3Key, expiresInSeconds = 300) {
const command = new GetObjectCommand({
Bucket: 'your-app-images',
Key: s3Key
const signedUrl = await getSignedUrl(s3Client, command, {
expiresIn: expiresInSeconds
return signedUrl;
过期时间建议:用户头像5分钟,合同文件30秒,下载链接10分钟。根据场景调,太短用户体验差,太长有泄露风险。
预签名URL的生成是计算密集型操作吗?不是。它只是本地做HMAC签名,不调用AWS API,可以放在高频接口里。
第六步:IAM最小权限原则
你的Node.js应用需要IAM凭证来操作S3。千万别用根账号密钥,要创建专用IAM用户,并绑定最小权限策略。
"Version": "2012-10-17",
"Statement": [
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::your-app-images/*"
注意Resource末尾的/*。如果写成arn:aws:s3:::your-app-images,策略对桶内对象不生效,这是新手常见坑。
生产环境建议用IAM Role而不是长期凭证。如果部署在EC2或ECS,直接绑定Role;如果是Lambda,用执行角色。这样密钥不会出现在代码或环境变量里。
一个容易忽略的细节:Content-Type
上传时如果不指定Content-Type,S3会默认application/octet-stream。这会导致浏览器下载图片而不是直接展示,用户体验崩掉。
务必从上传请求中读取MIME类型,或者通过文件魔数(file-type库)检测,显式设置Content-Type。
另外,如果你用了CloudFront做CDN,记得配置缓存行为,让Content-Type参与缓存键。否则不同格式的同名文件会互相污染缓存。
这套架构跑通后,一个中等规模的UGC平台(日活50万,人均3张图)的存储成本大概在每月200-400美元区间,取决于访问频率和是否走CDN。相比自建MinIO集群加运维人力,这个账不难算。
你现在的图片存储方案是什么?有没有遇到过Referer被绕过、或者预签名URL过期时间调不准的坑?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.