教程 / 后端集成 / 将文件上传至 S3 或 Cloudflare R2
📝 文字 ● 中级 更新于 2026-05-13

将文件上传至 S3 或 Cloudflare R2

为你的应用添加文件上传功能——头像、附件、导出文件,随你所需。同一份代码同时兼容 AWS S3 和 Cloudflare R2,按需选择最适合钱包的方案。浏览器直传加预签名 URL 是默认推荐模式。

S3 还是 R2——怎么选

0

两者使用同一套 API。下面的代码对两者都适用;唯一的区别在于你指向哪个端点。

  • AWS S3 — 久经考验,与所有 AWS 服务深度集成。定价:存储约 $0.023/GB/月,加上 $0.09/GB 出口流量费。流量费才是大头;带宽密集型应用的成本会迅速飙升。S3 定价 ↗
  • Cloudflare R2 — 兼容 S3 API,无出口流量费。存储约 $0.015/GB/月,零出口费。非常适合公开提供图片/文件服务。R2 定价 ↗

新项目建议:优先选 R2,除非你已经深度绑定在 AWS 体系中。省下的出口流量费会随规模持续累积。

创建存储桶

1

S3:控制台 → S3 ↗Create bucket。名称须全球唯一(小写,不含下划线),区域选离用户最近的。

R2:Cloudflare 控制台 → R2 ↗Create bucket。名称在你的 Cloudflare 账户内唯一即可。

默认关闭公开访问权限。我们将通过服务端或签名 URL 来提供文件访问。

获取凭据

2

S3:创建一个拥有 AmazonS3FullAccess 权限(或限定到该存储桶)的 IAM 用户。IAM 控制台 ↗ → Users → Create,然后生成访问密钥。

R2:R2 控制台 → Manage R2 API Tokens → Create token。选择"Object Read & Write"并限定到你的存储桶。

两种方式都会给你两个值:Access Key IDSecret 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 可省略

安装 SDK

3

S3 和 R2 使用同一个包:

npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

Python 请用:pip install boto3。Go 请参考:aws-sdk-go-v2 ↗

初始化客户端

4
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

方案 A:服务端上传(简单,但较慢)

5

浏览器将文件发送到你的服务器,服务器再转发给 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 或上传流量较大时就不建议了——会占满服务器内存和带宽。

方案 B:预签名 URL(推荐)

6

服务端签发一次性 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 — 最先踩坑的地方

7

浏览器直传需要在存储桶上配置 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",就来改这里。

下发文件

8

你已将 key 存入数据库。有两种方式将文件提供给用户:

公开读取(免费、简单):

签名读取 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 在有效期内一次性可用。

服务端校验上传内容

9

预签名 URL 信任客户端。如果不加约束,用户可能把一个 5 GB 的可执行文件伪装成"profile.jpg"上传。两个实用的安全措施:

  • 约束预签名参数——在 PutObjectCommand 中设置 ContentTypeContentLength。S3/R2 会拒绝不匹配的上传。
  • 上传后校验——客户端告知完成后,服务端对该 key 发起 HEAD 请求,确认大小、MIME 类型,必要时检查文件内容。

对于图片,可以在上传后通过 Lambda/Worker 调用 sharp ↗ 进行压缩和安全处理。

不要将访问密钥暴露给浏览器。预签名 URL 是浏览器上传的唯一安全模式。如果你发现自己在客户端代码中实例化了 new S3Client({ credentials }),请立即停下——这些密钥会被打包进前端 bundle,被人爬取后用来刷爆你的 AWS 账单。

实用技巧

10

官方参考文档

下一步