基于 Vercel Blob 构建 S3 兼容的对象存储网关

基于 Vercel Blob 构建 S3 兼容的对象存储网关

将Vercel Blob包装为支持S3的兼容网关,实现对象存储与Twikoo图床接入
背景

博客评论系统 Twikoo 的图片上传依赖第三方图床,近期服务稳定性无法保证。

尝试

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

结果

转向 Vercel Blob,并为其构建了一层 S3 兼容网关。

问题的由来

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

候选方案的评估结果如下:

方案问题
Cloudflare R2需要绑定支付方式,本人验证无法通过
直接使用 Vercel Blob SDK仅支持服务端调用,浏览器端无法直接使用
AWS S3成本与账户管理开销与需求不匹配(因本人特殊原因无法使用)
自建图床运维成本过高

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

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

解决思路有二:编写一个专用的上传转发接口,或者将 Blob 完整封装为 S3 兼容服务。后者具备更普遍的复用价值,于是实现了本项目:

设计与实现

项目在 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 请求。

完整请求链路如下:

text
浏览器 (Twikoo imgUploader)
  │  S3 模式:@aws-sdk/client-s3 自动计算 SigV4 签名
  │  HTTP 模式:请求头携带 Authorization: Bearer <token>
  ▼
网关层 (Vercel Function / Cloudflare Worker)
  │  认证校验 → 解析路径 → 提取 bucket 与 key
  ▼
Vercel Blob (存储后端)
  │  返回公开 URL / 元信息
  ▼
浏览器 → Twikoo 渲染图片

两个部署版本

基于 Next.js 15 App Router 的 Route Handler 实现,通过官方 @vercel/blob SDK 访问存储,签名校验使用 node:crypto 完成 HMAC 计算,并以 timingSafeEqual 进行时序安全的比较。首页内置拖拽上传页面,可用于日常手动传图。

限制:Serverless 存在冷启动,请求体大小上限为默认的 4.5MB(推荐设置为4MB)。

两版功能基本对齐,选择依据为:需要网页上传入口或希望减少维护面,选 Vercel 版;对延迟与文件大小敏感,选 Cloudflare 版。

接入 Twikoo

推荐使用简单 HTTP 接口,前端无需引入 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 };
    },
  },
});

若调用方已具备 S3 客户端,也可将 endpoint 指向网关的 /s3 路径并启用 forcePathStyle,按标准 S3 协议读写。

环境变量

变量名必填说明
BLOB_READ_WRITE_TOKENVercel Blob 读写令牌
S3_ACCESS_KEYS3 签名校验用 AccessKey,自定义
S3_SECRET_KEYS3 签名校验用 SecretKey,自定义
UPLOAD_TOKEN简单接口的 Bearer Token,自定义

自定义密钥建议使用强随机值,可执行

openssl rand -hex 32
生成。

限制与注意事项

密钥暴露于浏览器端

前端直传意味着 Token 会出现在客户端代码中,无法回避。缓解因素是 Vercel Blob 的公开 URL 不可枚举,读取无权限要求,整体风险可控。对安全性要求更高的场景,应将上传逻辑移至服务端,网关仅对内开放。

其余已知限制:

  • S3 接口仅覆盖对象级操作,不支持 ListObjects 等 bucket 级 API——网关没有真实的 bucket 概念。
  • 上传关闭了 addRandomSuffix,文件名依赖时间戳区分,理论上存在冲突可能。
  • 免费额度为 Blob 的 1GB 存储与每月 10GB 带宽;博客评论区场景下通常不会触及上限。

结语

仓库中的 vercel/DEPLOY.mdcf/DEPLOY.md 包含从零开始的部署步骤、环境变量配置与常见问题排查。如有类似需求——不限于 Twikoo,任何需要在浏览器端使用 Vercel Blob 的场景——欢迎使用或提交 issue。

本站评论图床已切换,欢迎在评论区验证。

这应该是开学前的最后一篇了awa. 之后我会出一篇详细部署文章,敬请期待.

站点无限期维护通知
已抵达博客尽头

评论区

评论加载中...