把 Vercel Blob 改造成 S3 兼容图床

把 Vercel Blob 改造成 S3 兼容图床

把 Vercel Blob 包装成 S3 兼容 + 简单 HTTP 双接口的对象存储网关,接入 Twikoo 评论图床
背景

博客评论系统 Twikoo 的图片上传依赖smms图床,但是smms不免费了:(

尝试

考虑迁移至 Cloudflare R2,支付方式绑定受限。

结果

转向 Vercel Blob,通过构建了一层 S3 兼容网关,进行转换。

问题的由来

本站评论区由 Twikoo 驱动,其图片上传通过 imgUploader 回调实现,存储需要自行对接图床。此前一直使用 sm.ms,近期该服务可靠性明显下降:已上传的图片在数日后出现失效,且无明确公告。免费第三方图床的可持续性本就存疑,迁移势在必行。

排除以上选项后,Vercel Blob 成为最合适的存储后端:开通即用,免费额度为 1GB 存储与每月 10GB 带宽,与本站评论区的图片量级完全匹配;且博客本身部署于 Vercel,存储与计算处于同一生态。

但它存在一个关键障碍:Vercel Blob 的 API 与 S3 不兼容——既不支持 S3 的 XML 协议,也不支持 SigV4 签名认证,官方 SDK 又只能在服务端运行。而 Twikoo 的上传行为发生在浏览器端,存储能力无法直接接入。

解决思路有二:编写一个专用的上传转发接口,或者将 Blob 完整封装为 S3 兼容服务。后来改名为 blob-s3-imgbed,并迁到了独立仓库:

  • S3 兼容接口:支持 PUT / GET / HEAD / DELETE,自带 AWS SigV4 签名校验,Twikoo 的「S3 / R2 / MinIO」插件可直接对接
  • 简单 HTTP 接口:POST /api/upload(Bearer Token)+ GET /api/download/{key}(302 重定向 / 流式代理),无需任何 SDK
  • 内置上传页面:访问根路径即可拖拽上传图片,一键复制绑定域名后的回调 URL
  • 零数据库:Bucket 仅作逻辑前缀,对象真实存放在 Vercel Blob
  • 签名安全:AccessKey / SecretKey 自定义,用 timingSafeEqual 常量时间比较;上传 Token 同样常量时间校验
  • 支持 Range:下载接口支持断点续传 / 视频拖拽

设计与实现

项目在 Vercel Blob 之上构建网关层,对外同时提供两套接口,共享同一个 Blob Store 后端:

  • S3 兼容接口:路径形如 /s3/{bucket}/{key},支持 PUT、GET、HEAD、DELETE 四类对象操作,认证采用标准 SigV4 签名校验。任何使用 @aws-sdk/client-s3 或 AWS CLI 的调用方均可零改动接入。网关本身没有 bucket 实体,bucket 仅作为路径前缀用于逻辑分组,对象实际存储于 Blob 中 {bucket}/{key} 对应的位置。
  • 简单 HTTP 接口:上传为携带 Bearer Token 的 fetch POST 请求;下载默认以 302 重定向至 Blob CDN 地址,附加 ?proxy=1 参数则切换为流式代理,隐藏源地址并支持 Range 请求。

完整请求链路如下:

技术栈:Next.js 15 App Router(Route Handler)+ @vercel/blob SDK + node:crypto。SigV4 校验是自己写的一套 HMAC 计算,没有引入 AWS SDK;签名比较使用 timingSafeEqual,避免时序侧信道。

仓库目录结构
text
.
├── app/
│   ├── layout.tsx                      # 根布局(metadata / html 骨架)
│   ├── page.tsx                        # 网页上传页(拖拽上传 + 复制 URL)
│   ├── s3/[...key]/route.ts            # S3 兼容接口(PUT/GET/HEAD/DELETE + SigV4)
│   └── api/
│       ├── upload/route.ts             # 简单上传接口(raw body / multipart)
│       └── download/[...key]/route.ts  # 简单下载接口(302 / 流式代理)
├── lib/
│   └── sigv4.ts                        # AWS SigV4 签名校验实现
├── .env.example                        # 环境变量模板
├── DEPLOY.md                           # 详细部署文档
├── TWIKOO_S3_CONFIG.md                 # Twikoo 管理面板 S3 插件配置指南
└── next.config.mjs

部署到 Vercel

新版把 Next.js 应用挪到了仓库根目录,Import 仓库后 Root Directory 保持默认即可,不需要再填 vercel 之类的子目录,Framework Preset 会自动识别为 Next.js。

  1. 打开 Vercel Dashboard → Import 该仓库
  2. 在 Settings → Environment Variables 里填入下面的变量
  3. 点击 Deploy

不想点面板也可以用 CLI:

sh
npm install -g vercel   # 如未安装
vercel                  # 首次部署(preview 环境)
vercel --prod           # 部署到生产环境

本地开发则是 npm install → 复制 .env.example 为 .env.local → npm run dev。

环境变量

变量名必填说明
BLOB_READ_WRITE_TOKEN是Vercel Blob 读写令牌。Blob Store 绑到同一项目时会自动注入,跨项目需手动填写
S3_ACCESS_KEY是S3 签名校验用 AccessKey,自定义值(如 twikoo-blob)
S3_SECRET_KEY是S3 签名校验用 SecretKey,强随机字符串
UPLOAD_TOKEN是简单上传接口(/api/upload)的 Bearer Token,强随机字符串

密钥建议用强随机值:openssl rand -hex 32

本地开发要手动填 BLOB_READ_WRITE_TOKEN

Vercel 不会把 Blob 令牌注入本地环境,npm run dev 之前需要自己复制一份:Vercel Dashboard → Storage → Blob Store → Copy Blob Read Write Token。

接口参考

需要 SigV4 签名,任何 S3 客户端都能零改动接入:

方法路径说明
PUT/s3/{bucket}/{key}上传对象
GET/s3/{bucket}/{key}下载对象(支持 Range)
HEAD/s3/{bucket}/{key}获取对象元信息
DELETE/s3/{bucket}/{key}删除对象(幂等)

用 AWS CLI 试一下最直观:

sh
aws configure   # region 填 auto,AccessKey/SecretKey 用 S3_ACCESS_KEY / S3_SECRET_KEY

aws s3 cp test.jpg --endpoint-url https://<你的域名>/s3 s3://comments/test.jpg
aws s3 cp --endpoint-url https://<你的域名>/s3 s3://comments/test.jpg downloaded.jpg

接入 Twikoo

推荐用管理面板的 S3 插件

Twikoo 的「S3 / R2 / MinIO」插件把配置存在服务端,密钥不会进入浏览器;前端 imgUploader 则必须把 UPLOAD_TOKEN 写进页面,只适合自用或内部场景。

方式适用场景说明
S3 插件(推荐)管理面板可视化配置密钥保存在 Twikoo 服务端,不暴露给浏览器,详见仓库的 TWIKOO_S3_CONFIG.md
前端 imgUploader自定义前端上传在博客页面里用 fetch 调 /api/upload,UPLOAD_TOKEN 会暴露在前端
网页手动上传偶尔上传几张图直接访问站点根路径拖拽上传

S3 插件里最关键的几项:

配置项值
S3_ENDPOINThttps://<你的域名>/s3
S3_FORCE_PATH_STYLEtrue
S3_CDN_URLhttps://<你的域名>/api/download,不含 bucket(填错会导致上传成功但图片不显示)
S3_REGIONus-east-1(任意值均可)
备选:前端 imgUploader 的写法

前端的 imgUploader 回调长这样,好处是页面里不用引入 S3 SDK:

twikoo.initjs
twikoo.init({
  envId: '<你的 envId>',
  imgUploader: {
    async upload(file) {
      const res = await fetch(
        'https://<网关域名>/api/upload?name='
        + encodeURIComponent(file.name) + '&path=comments',
        {
          method: 'POST',
          headers: {
            'Authorization': 'Bearer <UPLOAD_TOKEN>',
            'Content-Type': file.type,
          },
          body: file,
        }
      );
      const data = await res.json();
      return { url: data.url };
    },
  },
});

代价是 UPLOAD_TOKEN 会出现在客户端代码里,无法回避;缓解因素是 Vercel Blob 的公开 URL 不可枚举、读取无需权限,整体风险可控。要求更高的场景应把上传逻辑移到服务端,网关仅对内开放。

限制与费用

以下要点整理自 Vercel Blob Pricing(官方页面更新于 2026-09-23)。Blob 按区域定价,官方也可能调整,请以文档页面为准。

Hobby(免费版)额度

资源免费额度超出后
Blob 存储容量1 GB / 月有点少(4MB图片约250张,实际上每次约1-2MB)
Simple Operations前 10,000 次Hobby计划超限停用,30天重置
Advanced Operations前 2,000 次同上
Blob Data Transfer前 10 GB同上

超限不会自动扣费,只会收到提醒邮件并暂停 Blob 功能。Edge Requests、Fast Origin Transfer 按标准 CDN 费率另计,且免费额度在项目内所有 Vercel 服务间共享。

本项目会消耗哪些额度

操作计费项
PUT /s3/{bucket}/{key}、POST /api/upload1 次 Advanced Operation(服务端接收上传还会产生 Fast Data Transfer)
HEAD / GET / DELETE /s3/...内部调用 head(),各计 1 次 Simple Operation
DELETE 的 del() 本身免费,但计入速率限制
浏览器加载 Blob 公开 URL(302 后的地址)cache MISS 计 1 次 Simple Operation;无论 HIT/MISS,每次访问都计 1 次 Edge Request
?proxy=1 或 /s3 GET 流式代理额外的 Functions 侧数据传输费用

省额度的三条建议:

  • 优先走 /api/download/{key} 的 302 重定向:浏览器直连 Blob CDN,Blob Data Transfer 平均比 Fast Data Transfer 便宜约 3 倍。
  • 避免高频 HEAD 探测,每次都是 1 次 Simple Operation。
  • 把 Blob 当图片存档。
Pro 版参考价与速率上限

以 iad1 区域为例:

资源套餐内含超出单价
存储容量5 GB$0.023 / GB-month
Simple Operations100,000 次$0.40 / 百万次
Advanced Operations10,000 次$5.00 / 百万次
Blob Data Transfer100 GB$0.05 / GB
Fast Origin Transfer100 GB$0.06 / GB

请求速率上限:

限制HobbyProEnterprise
Simple Operations1,200 / 分钟(20/s)7,200 / 分钟(120/s)9,000 / 分钟(150/s)
Advanced Operations900 / 分钟(15/s)4,500 / 分钟(75/s)7,500 / 分钟(125/s)

其余几条硬限制:

  • 上传请求体上限约 4.5 MB:上传要经 Vercel Function 中转,受 Serverless 请求体限制约束;更大文件需改用 Blob 客户端直传。
  • 单 Blob 缓存上限 512 MB:超过后永不被 CDN 缓存,每次访问都会同时产生 Simple Operation 与 Fast Origin Transfer 费用。
  • 单文件最大 5 TB,官方建议超过 100 MB 用 multipart 上传。
  • 区域固定:Blob Store 可在 19 个区域创建,创建后不可更改,各区域价格不同。
  • 控制台操作也计费:在 Vercel Dashboard 浏览文件列表、上传文件、查看详情都会计入 Operations。
  • S3 接口只覆盖对象级操作:不支持 ListObjects 等 bucket 级 API —— 网关没有真实的 bucket 概念。
  • 上传关闭了 addRandomSuffix,文件名依赖时间戳区分,理论上存在冲突可能。

常见问题

访问 /api/upload 返回 401?

UPLOAD_TOKEN 没配置,或请求头里的 Authorization: Bearer <token> 与环境变量不一致。

S3 客户端报 SignatureDoesNotMatch?

依次检查:S3_ACCESS_KEY / S3_SECRET_KEY 是否与客户端一致;endpoint 是否为 https://<域名>/s3(不要带尾部斜杠);是否设置了 forcePathStyle: true。

上传成功但图片不显示?

Twikoo 场景下通常是 S3_CDN_URL 留空或填错,应填 https://<域名>/api/download,不含 bucket。

能上传多大的文件?

Max约4.5 MB —— 这是 Vercel Serverless Function 的请求体上限。更大的文件建议直接用 Vercel Blob 客户端走API。

免费额度用完了会怎样?

Hobby 版含 1 GB 存储、1 万次 Simple Operations、2 千次 Advanced Operations、10 GB 流量;超限只会暂停 Blob 功能(不扣费),需等 30 天额度重置或升级 Pro。

结语

完...

站点双评论系统简记
Linux 35岁生日快乐!

评论区

评论加载中...