今天终于把图床的 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 图床防刷方案就完成了。