
Cloudflare 全家桶以其强大的基础服务和慷慨的免费套餐,正成为越来越多开发者构建 Web 应用的基石。从 R2 对象存储、Workers 计算到 Pages 静态网站托管,Cloudflare 提供了一站式的解决方案,尤其对于起步阶段的项目,极大地降低了初期投入和后期运维的门槛。
我们团队之前的项目,前端普遍采用 Vue3 构建,编译打包后直接部署到 Cloudflare Pages。配合自定义域名解析,不仅轻松实现了 HTTPS 和 CDN 加速,后续维护也几乎是“零操心”模式。
最近,在设计开发一个基于 OpenAI API 的 AIGC(图生图、文生图)Web 应用时,我们遇到了一个核心需求:如何安全、可靠、低成本地长期存储用户上传和 AI 生成的图片?答案几乎是显而易见的——Cloudflare R2。
Cloudflare R2 Free tier(免费套餐):
Free | |
---|---|
Storage | 10 GB-month / month |
Class A Operations | 1 million requests / month |
Class B Operations | 10 million requests / month |
Egress (data transfer to Internet) | Free |
- Class A 操作:通常指写入、修改类操作 (如 PutObject, CopyObject)。
- Class B 操作:通常指读取类操作 (如 GetObject)。
免费的出口流量尤其亮眼,这意味着用户访问存储在 R2 上的资源(如图图片)时,我们无需担心高昂的流量费用。
R2 支持绑定自定义域名。最简单的方式是将你的域名(或子域名,如 images.yourdomain.com)通过 CNAME 解析到 R2 分配的公共访问地址 (public.your-bucket.your-account-id.r2.cloudflarestorage.com)。设置完成后,你就可以通过 https://images.yourdomain.com/your-object.jpg
这样的 URL 来访问 R2 资源了。
然而,便利往往伴随着风险。 这种直接 CNAME 的方式相当于将你的 R2 存储桶“裸奔”在公网上,没有任何权限验证。任何人只要知道你的域名和对象键(文件名),就能直接访问甚至盗用你的资源。对于我们这个 AIGC 应用来说,用户上传和生成的图片往往具有一定的私密性,必须进行严格的访问控制。防盗链、身份验证、访问时效性判断,这些都是必不可少的安全措施。
那么,如何在享受 R2 自定义域名便利的同时,确保资源的安全性呢?
方案一: 预签名URL - 安全但有限制
Cloudflare R2 兼容 AWS S3 API。这意味着我们可以利用 Cloudflare API 或兼容 S3 的 SDK(如 AWS SDK for Go/Java/Python 等)为存储桶中的特定对象生成一个临时的、带有签名凭证和过期时间的访问 URL。
实现流程大致如下:
- 前端请求:前端页面加载需要的某个图片。
- 后端生成签名: 前端向后端服务(例如,一个 Go 语言实现的 RESTful API)发起请求,告知需要访问哪个图片。
- 后端映射R2对象: 后端服务检索图片,映射到对应 R2 对象。
- 后端签名: 后端服务使用 R2 的访问密钥(Access Key ID 和 Secret Access Key)为该对象生成一个预签名 URL。这个 URL 包含了访问凭证和设定的过期时间(例如,5 分钟后失效)。
- 返回 URL: 后端服务将生成的预签名 URL 返回给前端。
- 前端访问: 前端拿到这个临时的、带签名的 URL,通过 HTML 的
<img>
标签或其他方式去请求 R2 资源。
优点:
- 安全性高: 每个 URL 都是临时的,带有访问凭证和过期时间,有效防止未授权访问和资源盗用。
- 控制精细: 可以为每个对象、每次请求生成独立的访问凭证。
缺点:
- 无法使用自定义域名: 生成的预签名 URL 强制使用 R2 的原始访问域名 (your-bucket.your-account-id.r2.cloudflarestorage.com)。如果希望通过自己的品牌域名(如 images.mydomain.com)来访问,并隐藏底层的 R2 桶信息,这种方式就无法满足需求了。
对于有一致性和隐藏底层实现细节的场景,预签名 URL 显然不是完美的解决方案。
方案二:Cloudflare Workers - 自定义域名访问控制
既然预签名 URL 无法满足自定义域名的需求,那有没有一种方法可以让我们鱼与熊掌兼得呢?答案是肯定的:Cloudflare Workers。
Cloudflare Workers 允许我们在 Cloudflare 的边缘节点上运行 JavaScript 代码,拦截并处理 HTTP 请求。
我们可以将自定义域名(如 images.mydomain.com)指向一个部署好的 Worker,这个 Worker 就如同一个智能的“守门人”,负责:
- 接收所有通过自定义域名发往 R2 资源的请求。
- 验证请求的合法性(例如,检查 Token、校验来源、判断权限等)。
- 如果验证通过,则由 Worker 内部去访问实际的 R2 存储桶获取资源。
- 将从 R2 获取的数据流式返回给原始请求的客户端。
这样一来,对外的接口始终是我们的自定义域名,而底层的 R2 访问和权限控制逻辑则被封装在 Worker 内部,完美解决了预签名 URL 的限制。
策略A:基于HTTP Header的Token认证(JWT-like)
这是我最初尝试的思路,借鉴了常见的 API 认证方式。
实现流程:
- 域名指向: 确保自定义域名 images.mydomain.com 已解析并指向部署好的 Cloudflare Worker。
- 前端请求资源: 前端需要访问 R2 上的 image001.png。
- 获取 Token: 前端首先从后端服务(或专门的授权服务)获取一个有时效性的认证 Token(类似于 JWT,可以包含用户身份、权限、过期时间等信息)。
- 携带 Token 请求: 前端在请求
https://images.mydomain.com/image001.png
时,将获取到的 Token 放入 HTTP 请求的 Header 中,通常是 Authorization: Bearer <your_token>。 - Worker 拦截与验证: Cloudflare Worker 接收到请求,从 Authorization Header 中提取 Token。
- Worker 校验: Worker 验证 Token 的签名、检查是否过期、解析其中包含的权限信息等。
- Worker 代理访问 R2: 验证通过后,Worker 内部构造指向 R2 存储桶中 image001.png 的实际请求(需要配置 Worker 绑定 R2 存储桶的权限),并从 R2 获取数据。
- Worker 返回响应: Worker 创建一个新的 HTTP Response,将从 R2 获取到的数据流式传输回给前端。
遇到的问题:
这个方案在逻辑设计和 Worker 代码实现上都没有问题。然而,在前端集成时却遇到了一个问题:浏览器原生 <img>
标签的限制。
项目中,图片通常是直接使用 HTML 的 <img>
标签来加载的:
<img src="https://images.mydomain.com/image001.png" alt="Image">
浏览器在处理 <img>
标签发出的图片请求时,通常不允许我们像使用 fetch 或 XMLHttpRequest 那样,方便地自定义添加 Authorization 这样的 HTTP Header。
这意味着,如果坚持使用 Header 传输 Token,前端的图片加载方式就需要做较大改动:
- 不能直接使用
<img>
的src
属性。 - 需要使用 fetch API 发起请求,并在 Header 中带上 Token。
- 将 fetch 返回的响应体(Response Body)转换成 Blob 对象。
- 使用 URL.createObjectURL() 为 Blob 对象生成一个临时的本地 URL。
- 最后将这个 Blob URL 赋值给
<img>
标签的src
属性。
// 示例:使用 fetch 加载带 Token 的图片
const imageUrl = 'https://images.mydomain.com/image001.png';
const token = 'your_bearer_token';
fetch(imageUrl, {
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => response.blob())
.then(blob => {
const objectURL = URL.createObjectURL(blob);
const imgElement = document.getElementById('myImageElement'); // 获取<img>元素
imgElement.src = objectURL;
})
.catch(error => console.error('Error loading image:', error));
虽然可行,但这无疑增加了前端的复杂性,也改变了我们习惯的图片加载方式,对于现有代码的兼容性也不友好。
策略B:基于URL查询参数的Token认证
为了继续沿用简洁的 <img>
标签加载方式,我调整了策略,将认证 Token 从 HTTP Header 移到了 URL 的查询参数(Query Parameter)中。
实现流程:
- 域名指向: 同样,自定义域名 images.mydomain.com 指向 Cloudflare Worker。
- 前端请求资源: 前端需要访问 R2 上的 image001.png。
- 获取带 Token 的 URL: 前端向后端服务请求图片的访问 URL。后端服务生成一个有时效性的 Token,并将其附加到图片的 URL 后面作为查询参数,形成类似
https://images.mydomain.com/image001.png?token=xxxxxxx
的格式。 - 后端返回 URL: 后端将这个带有 Token 的完整 URL 返回给前端。
- 前端直接使用 URL: 前端拿到这个 URL 后,可以直接将其赋值给
<img>
标签的src
属性。 - Worker 拦截与验证: Cloudflare Worker 接收到请求,从 URL 的查询参数中提取 token。
- Worker 校验: Worker 验证 token 的有效性(解密、签名校验、检查过期时间等)。
- Worker 代理访问 R2: 验证通过后,Worker 忽略查询参数部分,根据路径 (/image001.png) 从绑定的 R2 存储桶获取对象数据。
- Worker 返回响应: Worker 将从 R2 获取的数据流式返回给前端。
<img src="https://images.mydomain.com/image001.png?token=xxxxxxx" alt="Image">
优点:
- 前端友好: 对前端代码的侵入性最小,可以继续使用标准的
<img>
标签,无需修改图片加载逻辑。 - 实现简单: 整体流程相对直观。
潜在缺点:
- Token 暴露在 URL 中: Token 会出现在浏览器地址栏、访问日志、Referrer 头等地方,相对 Header 传输,安全性略低。但通过设置较短的 Token 有效期(例如几分钟),可以有效缓解这个问题。
考虑到我们项目中对前端简洁性的要求,以及 Token 短时效可以接受的安全模型,我们最终选择了基于 URL 查询参数的 Token 认证方案。
总结
Cloudflare R2 提供了极具吸引力的对象存储服务,尤其是其慷慨的免费额度和免出口流量费策略。然而,直接通过 CNAME 绑定自定义域名并公开访问存在严重的安全隐患。
为了实现对 R2 自定义域名的安全访问控制,我们探索了两种主流方案:
- 预签名 URL: 安全性高,控制粒度细,但无法与自定义域名直接结合使用,会暴露 R2 的原始域名。
- Cloudflare Workers 代理: 可以完美结合自定义域名和访问控制。
- Header 认证: 理论上更符合 API 认证规范,但与原生
<img>
标签存在兼容性问题,需要前端采用更复杂的加载方式。 - URL 参数认证: 对前端最友好,可直接用于
<img>
标签,实现简单。需注意 Token 时效性管理以降低暴露风险。
最终,基于对前端易用性的考量,我们采用 Cloudflare Workers 结合 URL 参数 Token 的方式,成功构建了一套既能使用自定义域名,又能精细控制访问权限的 R2 资源服务。
这再次证明了 Cloudflare 生态系统内部组件(R2 + Workers)组合的强大威力与灵活性。