返回博客

国内云服务器 fetch 大厂 CDN 永远 403:datacenter IP 黑名单才是真凶

某次做资源归档功能,逻辑很直接:用户在桌面 App 里抓到第三方平台的笔记封面 URL,POST 给后端,后端 fetch 图片字节、写进对象存储、入库。本地开发跑得很顺,上线后一测——DB 里 cover_oss_key 全是 NULL,封面一片空白。

看日志,后端 fetch 返回 403。加 Referer 头,还是 403。换真实浏览器 UA,还是 403。加登录态 Cookie……还是 403。

现象

  • 同一张图片 URL,在开发机(家庭宽带)用 curl 带 Referer 头拉:200 OK,bytes 正常
  • SSH 到生产 ECS(阿里云北京),同样命令:403
  • 换腾讯云、华为云节点测试:一样 403
  • URL 本身没过期(token 有效期通常 7 天)

这两个对比几乎是决定性证据。

根因

中国大厂 CDN(小红书、微信、抖音、微博等)普遍实施双层反爬

第一层:Referer 防盗链——请求必须带合法的来源域(如 xiaohongshu.com)。这层好绕,加 Referer 头即可,不是问题所在。

第二层:datacenter IP 黑名单——阿里云、腾讯云、华为云、AWS-cn、Azure-cn 等云服务商的公网 IP 段,整段被列入拒绝名单。CDN 厂商定期更新这张黑名单,服务器 IP 无论怎么伪装请求头都突破不了。

坑:本地测通了不代表生产能用

开发机在家庭宽带(住宅 IP),生产服务器在云机房(datacenter IP)。两者出口 IP 类型不同,行为完全不同。任何依赖出口 IP 的功能,必须在生产 IP 段验证通过才算修好。

"本地测试通过"这句话在这种场景里是陷阱。

更糟的是,失败常常被 try/catch 静默吞掉,日志只写 ok=0/N,UI 用原始 CDN URL 兜底展示——看起来功能"正常",实则归档完全失效,bug 永远不会被发现。

正解

不能在服务端 fetch CDN,必须把 fetch 翻转到用户本机执行。

死路架构(经典)

桌面 App / 浏览器
    ↓ POST { coverUrl }
服务端(云机房 datacenter IP)
    ↓ fetch(coverUrl, { Referer }) ← 永远 403
    ↓ 写对象存储                    ← 走不到这一步
DB cover_oss_key 永远 NULL

正确架构:sidecar 端 fetch + bytes 上传

桌面 App sidecar(用户本机·住宅 IP)
    ├─ fetch(coverUrl, { Referer })  ← 住宅 IP,CDN 放行
    ├─ buffer.toString('base64')
    └─ POST /api/upload-cover  body: { id, contentType, bytesBase64 }
       服务端(只负责 OSS 写入)
         ├─ Buffer.from(base64)
         ├─ objectStore.put(...)
         └─ DB UPDATE cover_oss_key

关键原则:谁有住宅 IP,谁负责 fetch CDN;服务端只接字节、写存储。

正解:fetch 发生在 sidecar,服务端只写 OSS

把 fetch CDN 这一步从服务端移到桌面 App 的 sidecar 进程中执行。sidecar 用用户的住宅 IP 拉到字节后,base64 编码 POST 给服务端;服务端只做 Buffer.from + 对象存储写入 + 更新 DB,不再自己 fetch CDN。

防止失败死循环:永久失败标记

存量待归档记录需要补跑,但要区分「从未尝试」和「已彻底失败」,否则每次用户打开页面都触发 retry → 403 → retry 的死循环:

ALTER TABLE resource_archive
  ADD COLUMN archive_status   TINYINT NOT NULL DEFAULT 0,
  -- 0=pending, 1=ok, 2=permanent_fail
  ADD COLUMN archive_fail_count TINYINT NOT NULL DEFAULT 0;

失败语义:

  • 404 / 410:一次性标 status=2(URL 已失效,不再 retry)
  • 403 / 429 / 5xx / 网络超时fail_count+1,累计 ≥3 才标 status=2
  • 成功:status=1, fail_count=0

补跑 SQL 条件加上 AND archive_status != 2,确保永久失败的不会被重复触发。

fail-loud 而非 fail-silent

失败时不要 try/catch 静默吞掉、用原始 CDN URL 兜底——那等于让 bug 永远不可见。应该:UI 加「封面归档失败」badge、日志打 warn、DB 记录失败状态。让错误在系统里留下可追踪的痕迹。

一句话外卖

中国云机房 IP 向国内大厂 CDN 发请求,结构性地被 datacenter IP 黑名单拦死;fetch CDN 必须发生在用户本机(住宅 IP),服务端只接字节写存储,本地测通不等于生产可用。