为你的应用添加文件上传功能——头像、附件、导出文件,随你所需。同一份代码同时兼容 AWS S3 和 Cloudflare R2,按需选择最适合钱包的方案。浏览器直传加预签名 URL 是默认推荐模式。
S3:创建一个拥有 AmazonS3FullAccess 权限(或限定到该存储桶)的 IAM 用户。IAM 控制台 ↗ → Users → Create,然后生成访问密钥。
R2:R2 控制台 → Manage R2 API Tokens → Create token。选择"Object Read & Write"并限定到你的存储桶。
两种方式都会给你两个值:Access Key ID 和 Secret Access Key。将它们存入后端环境变量:
S3_ACCESS_KEY_ID=AKIAxxxxxxxxxxxxxxxx
S3_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
S3_BUCKET=my-app-uploads
S3_REGION=auto # R2 使用 "auto";S3 使用如 "us-east-1"
S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com # 仅 R2 需要;S3 可省略
S3 和 R2 使用同一个包:
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
Python 请用:pip install boto3。Go 请参考:aws-sdk-go-v2 ↗。
const { S3Client } = require("@aws-sdk/client-s3");
const s3 = new S3Client({
region: process.env.S3_REGION,
endpoint: process.env.S3_ENDPOINT, // omit for AWS S3
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
});
这个客户端同时适用于 S3 和 R2。唯一的区别是是否设置 endpoint。
浏览器将文件发送到你的服务器,服务器再转发给 S3/R2。逻辑最简单,但每个字节都要经过你的服务器(大文件速度慢,Serverless 环境成本高)。
const { PutObjectCommand } = require("@aws-sdk/client-s3");
const multer = require("multer");
const upload = multer({ storage: multer.memoryStorage() });
app.post("/api/upload", upload.single("file"), async (req, res) => {
const key = `uploads/${Date.now()}-${req.file.originalname}`;
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
Body: req.file.buffer,
ContentType: req.file.mimetype,
}));
res.json({ key });
});
可以用,但文件超过约 10 MB 或上传流量较大时就不建议了——会占满服务器内存和带宽。
服务端签发一次性 URL,让浏览器直接上传到 S3/R2。服务器完全不接触文件内容。
const { PutObjectCommand } = require("@aws-sdk/client-s3");
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
// 1) 浏览器向服务器请求上传 URL
app.post("/api/upload-url", async (req, res) => {
const { filename, contentType } = req.body;
const key = `uploads/${Date.now()}-${filename}`;
const url = await getSignedUrl(
s3,
new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
ContentType: contentType,
}),
{ expiresIn: 60 * 5 } // 5 minutes
);
res.json({ url, key });
});
然后在浏览器端:
// In the browser
async function uploadFile(file) {
const r = await fetch("/api/upload-url", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: file.name, contentType: file.type }),
});
const { url, key } = await r.json();
await fetch(url, {
method: "PUT",
headers: { "Content-Type": file.type },
body: file,
});
return key; // your app stores this in the DB
}
只需一次服务端往返来签发 URL,上传直接进行。文件大小不限;服务器保持轻量且无状态。
浏览器直传需要在存储桶上配置 CORS。否则浏览器会拦截 PUT 请求。
S3:存储桶 → Permissions → CORS:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["PUT", "POST", "GET"],
"AllowedOrigins": ["https://yourdomain.com", "http://localhost:3000"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]
R2:存储桶 → Settings → CORS Policy,配置内容相同。R2 CORS 文档 ↗。
如果浏览器控制台报"blocked by CORS policy",就来改这里。
你已将 key 存入数据库。有两种方式将文件提供给用户:
公开读取(免费、简单):
https://cdn.yourdomain.com/uploads/xxx.jpg——全球免费分发。签名读取 URL(用于私有文件):
const { GetObjectCommand } = require("@aws-sdk/client-s3");
const url = await getSignedUrl(
s3,
new GetObjectCommand({ Bucket: process.env.S3_BUCKET, Key: key }),
{ expiresIn: 60 * 60 } // 1 hour
);
适用于用户专属内容(私密文档、付费下载)。URL 在有效期内一次性可用。
预签名 URL 信任客户端。如果不加约束,用户可能把一个 5 GB 的可执行文件伪装成"profile.jpg"上传。两个实用的安全措施:
PutObjectCommand 中设置 ContentType 和 ContentLength。S3/R2 会拒绝不匹配的上传。HEAD 请求,确认大小、MIME 类型,必要时检查文件内容。对于图片,可以在上传后通过 Lambda/Worker 调用 sharp ↗ 进行压缩和安全处理。
new S3Client({ credentials }),请立即停下——这些密钥会被打包进前端 bundle,被人爬取后用来刷爆你的 AWS 账单。
XMLHttpRequest 代替 fetch 以获取上传进度事件。或者用 Uppy ↗,功能完整的上传组件。