记一次 EdgeOne + Cloudflare R2 图床防刷配置

图床 edgeone cloudflare

今天终于把图床的 CDN 和源站防护方案跑通了。

之前我的图片是放在 Cloudflare R2 上的。R2 本身很好用,便宜、稳定,也适合做图床存储。但是如果直接把 R2 的公开访问地址暴露出去,就会有一个隐患:别人可以绕过 CDN 直接请求 R2 源站,一旦被恶意刷访问,就可能产生大量读取请求。

所以这次我的目标很明确:

用户正常访问图片时走 EdgeOne CDN;
R2 源站不直接暴露;
直接访问 R2 源站会被 Cloudflare WAF 拦截;
EdgeOne 回源时带密钥,Cloudflare 校验通过后才允许访问 R2。

最终效果是:

直接访问 R2 源站:被 Cloudflare WAF 拦截
访问 EdgeOne 图床域名:图片正常打开

这说明配置基本成功了。


一、整体架构

这次的结构大概是这样:

用户
  ↓
img2.example.com
  ↓
EdgeOne CDN
  ↓
R2 自定义源站域名
  ↓
Cloudflare R2 Bucket

简单来说:

img2.example.com
= 对外访问的图床域名

r2-origin.example.com
= Cloudflare R2 绑定的自定义源站域名,只给 EdgeOne 回源使用

用户访问图片时,只访问:

https://img2.example.com/foot.webp

而不是直接访问 R2 源站。


二、Cloudflare R2:绑定自定义域,不启用公共开发 URL

在 Cloudflare R2 存储桶设置里,有两个和公开访问有关的地方:

自定义域
公共开发 URL

我这次选择的是:

使用自定义域
不启用公共开发 URL

公共开发 URL 类似:

xxx.r2.dev

这种地址比较适合测试,不太适合长期生产使用。为了后续配合 WAF、隐藏源站和统一访问入口,我没有启用它。

我给 R2 绑定了一个专门作为源站的自定义域,例如:

r2-origin.example.com

这个域名不直接发给用户,只给 EdgeOne 回源使用。


三、Cloudflare WAF:没有源站密钥就阻止访问

R2 源站域名绑定好以后,关键就是给它加 WAF 规则。

我的思路是:

如果访问的是 R2 源站域名,并且请求头里没有正确的密钥,就直接阻止。

Cloudflare WAF 自定义规则表达式类似这样:

http.host eq "r2-origin.example.com"
and not any(http.request.headers["x-origin-secret"][*] eq "你的随机密钥")

动作选择:

阻止 / Block

这条规则的意思是:

访问 r2-origin.example.com
但是没有携带正确的 x-origin-secret 请求头
就直接拦截

这里的随机密钥建议使用一串足够长的字符串,例如:

r2_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

不要使用太短、太容易猜的值。

另外要注意,Cloudflare WAF 表达式里请求头名称建议写小写:

x-origin-secret

而 EdgeOne 里可以写成:

X-Origin-Secret

HTTP 请求头本身不区分大小写。


四、EdgeOne:添加图床加速域名

接下来在 EdgeOne 里添加加速域名。

我的对外图床域名是:

img2.example.com

EdgeOne 回源配置大概这样填:

加速域名:
img2.example.com

源站类型:
IP/域名

源站地址:
r2-origin.example.com

回源协议:
HTTPS

回源端口:
443

回源 HOST:
使用源站域名

这里有一个很重要的点:

回源 HOST 不要使用加速域名,而要使用 R2 的源站自定义域名。

也就是说,EdgeOne 对外是:

img2.example.com

但回源时要请求:

r2-origin.example.com

否则 Cloudflare R2 可能无法正确识别源站域名,WAF 规则也可能匹配不上。


五、EdgeOne:添加回源请求头

因为 Cloudflare WAF 要校验 x-origin-secret,所以 EdgeOne 回源时必须带上同样的请求头。

在 EdgeOne 规则引擎里添加一条规则:

匹配条件:
HOST 等于 img2.example.com

然后添加操作:

操作:
修改 HTTP 回源请求头

类型:
设置

头部名称:
X-Origin-Secret

头部值:
你的随机密钥

注意这里一定要选:

修改 HTTP 回源请求头

不要选成响应头。

因为这个密钥只应该出现在 EdgeOne 到 Cloudflare R2 的回源请求里,不能暴露给普通用户。

最终逻辑就是:

普通用户访问 img2.example.com
→ EdgeOne 接收请求
→ EdgeOne 回源 R2 时自动带上 X-Origin-Secret
→ Cloudflare WAF 校验通过
→ R2 返回图片

而如果别人直接访问 R2 源站:

https://r2-origin.example.com/foot.webp

因为没有这个请求头,就会被 Cloudflare WAF 拦截。


六、EdgeOne:设置强缓存,减少 R2 B 类操作

防刷不只是拦源站,还要减少回源。

因为图床的图片一般上传后不会频繁变化,所以我给图片设置了比较长的节点缓存时间。

EdgeOne 规则引擎里设置:

匹配条件:
HOST 等于 img2.example.com

操作:
节点缓存 TTL

时间:
30 天

强制缓存:
开启

如果图片文件名是随机名、哈希名或者日期路径,例如:

/2026/06/foot.webp
/a8f3c92d.webp

其实也可以把缓存时间设置得更长,比如:

365 天

我目前先设置为 30 天,比较稳妥。

这样做的好处是:

第一次访问图片:
EdgeOne 回源 R2,获取图片并缓存

后续访问图片:
EdgeOne 直接返回缓存,不再回源 R2

这能明显减少 R2 的读取请求,也就是减少 R2 B 类操作压力。


七、EdgeOne:Cache Key 忽略查询字符串

这个设置也很重要。

如果 Cache Key 保留全部查询字符串,别人可以这样刷:

https://img2.example.com/foot.webp?a=1
https://img2.example.com/foot.webp?a=2
https://img2.example.com/foot.webp?a=3

虽然访问的是同一张图片,但 CDN 可能会把它们当成不同资源,导致缓存命中率下降,甚至增加回源。

所以我把 Cache Key 设置成:

查询字符串:
全部忽略

这样:

/foot.webp?a=1
/foot.webp?a=2
/foot.webp?a=3

都会被视为同一个缓存资源。

这对防止恶意随机参数刷缓存很有帮助。


八、EdgeOne:浏览器缓存设置

除了 EdgeOne 节点缓存,还可以给图片设置浏览器缓存。

我对常见图片格式设置了浏览器缓存:

jpg
jpeg
png
gif
bmp
svg
webp
avif
ico

浏览器缓存时间可以设置为:

7 天

如果图片基本不会替换,也可以设置:

30 天

这样用户重复打开文章或图片时,浏览器本地也能直接使用缓存,访问体验会更好。


九、PHP/JSP 等动态后缀不缓存

EdgeOne 默认规则里还有一条:

php
jsp
asp
aspx

这些动态脚本后缀不缓存。

对于 R2 图床来说,这些后缀一般也用不到。保留不缓存规则没什么问题。

如果想更严格,也可以在安全规则里直接拦截这些后缀访问。


十、最终测试结果

配置完成后,我测试了两个地址。

1. 直接访问 R2 源站

https://r2-origin.example.com/foot.webp

结果显示:

Sorry, you have been blocked

说明 Cloudflare WAF 已经成功拦截了直接访问源站的请求。

这一步很关键,说明源站不是裸奔状态。

2. 访问 EdgeOne 图床域名

https://img2.example.com/foot.webp

图片可以正常打开。

这说明 EdgeOne 回源时已经成功携带了:

X-Origin-Secret

Cloudflare WAF 校验通过,R2 正常返回图片。

最终状态就是:

直接访问源站:失败,被 WAF 拦截
访问 CDN 域名:成功,图片正常显示

这正是我想要的效果。


十一、这套配置能防什么?

这套方案主要能解决几个问题:

1. 防止别人直连 R2 源站

别人即使知道了源站地址,直接打开也会被 Cloudflare WAF 拦截。

2. 减少 R2 B 类操作

图片命中 EdgeOne 缓存后,不会每次都回源到 R2。

这样可以减少 R2 的读取类请求压力。

3. 防止随机参数刷缓存

通过 Cache Key 忽略查询字符串,可以避免别人用 ?a=1?a=2 这类随机参数制造大量不同缓存。

4. 方便以后迁移

对外只暴露:

img2.example.com

以后后端不管换成 R2、S3、对象存储、又拍云还是其他服务,只要保持图片路径不变,用户访问地址就可以尽量不变。


十二、后续还可以继续优化

目前基础防护已经跑通,后面还可以继续加几项:

1. EdgeOne 限频规则
2. 404 状态码缓存
3. Referer 防盗链
4. Token 鉴权
5. HTTPS 强制跳转
6. 图片压缩与格式转换

其中我比较想继续加的是:

404 状态码缓存

因为很多恶意访问不是刷已有图片,而是乱扫不存在的路径。

比如:

/test.jpg
/admin.png
/upload/1.webp
/random/abc.jpg

如果这些 404 请求每次都回源,也会消耗源站请求。

所以后续可以设置:

404 缓存 5 分钟或 10 分钟
403 缓存 10 分钟

这样乱扫路径时,EdgeOne 可以直接返回缓存的错误状态,不必每次都回源 R2。


十三、总结

这次配置的核心不是单纯“套一层 CDN”,而是:

EdgeOne 负责对外访问、缓存和防刷
Cloudflare WAF 负责保护 R2 源站
R2 只作为隐藏对象存储源站

最终实现的效果:

用户访问 img2.example.com:正常打开图片
攻击者直连 R2 源站:被 Cloudflare WAF 拦截
重复访问图片:尽量命中 EdgeOne 缓存
随机参数访问:不会轻易刷穿缓存

今天这个小配置还是挺有成就感的。

从一开始担心 R2 被刷 B 类操作,到最后看到源站被 Cloudflare 拦截、CDN 域名正常打开图片,说明整个链路已经打通了。

这也让我更清楚地理解了图床防护的重点:

不要让对象存储直接暴露在公网前面,
不要让所有请求都打到源站,
对外只暴露 CDN 域名,
源站只信任 CDN 回源。

这样,一个相对稳妥的 R2 图床防刷方案就完成了。