一个下载任务全线失败,HTTP 状态码清一色是 403 Forbidden。你的第一反应可能是"地址错了"或者"没有权限"。但这次,问题其实出在一个更朴素的地方:对方觉得你不像浏览器。
现象
下载某类流媒体分片时,CDN 持续返回 403,全部失败,一个都没成功:
$ downloader --url "https://<cdn-host>/segment/0001.ts"
HTTP error 403: Forbidden
HTTP error 403: Forbidden
HTTP error 403: Forbidden
...
地址本身是对的(浏览器里能播),网络也是通的,可命令行工具就是拿不到数据。
根因
CDN 会拦截命令行工具的默认 UA(User-Agent)。当你用 curl、wget 或某个下载器发请求时,它们默认会带上类似 curl/7.88.1 这样的 UA 字符串。CDN 的风控规则看到这种"明显不是浏览器"的 UA,就直接返回 403。
这就是为什么这个现象特别有迷惑性:403 通常让人联想到权限,可这里压根不是权限问题——它是一个反爬/反脚本的拦截动作,只是恰好用了 403 这个状态码。
排查的关键在于:如果你在浏览器里能正常访问同一个 URL,但命令行工具不行,那基本可以断定是 UA(或者其它请求头)的差异导致的。
解决
把请求 UA 换成常见的浏览器 UA。具体看工具支持哪种方式:
# 下载器支持 -user_agent 参数时
downloader --url "https://<cdn-host>/segment/0001.ts" \
-user_agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"
如果是自己用 Python 发请求,就手动设置请求头:
import requests
HEADERS = {
"User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0 Safari/537.36"),
"Referer": "https://<cdn-host>/", # 有些 CDN 还会校验 Referer
}
resp = requests.get(segment_url, headers=HEADERS, timeout=15)
resp.raise_for_status()
如果只改 UA 还不够,可以继续对照浏览器实际发出的请求头(按 F12 打开开发者工具,看 Network 面板),把 Referer、Accept、Accept-Language 等关键头补齐。这是整件事里唯一卡住过的点——UA 一换,下载就通了。
延伸与预防
这个案例的通用教训是:403 不一定是权限问题,可能只是"你不像浏览器"。
以后再遇到 403,建议按这个顺序排查:
- 先确认地址和网络——浏览器能不能打开?能,就排除"地址错"和"被墙";
- 对比请求头——把你的请求和浏览器的请求头逐项对照,重点看 UA、Referer、Cookie;
- 再考虑权限——如果是需要登录的资源,才轮到 token/Cookie 的问题。
预防上,可以给采集脚本准备一套"浏览器化"的默认请求头,作为所有请求的基线;同时把 403 当成"信号"而不是"结论"——它提示你请求被识别为非浏览器流量,解决方向是"伪装成正常流量",而不是去查权限配置。
还有一个值得记住的区分:UA 引起的 403 往往是"稳定复现"的——同一个 URL,用命令行永远 403,用浏览器永远成功,不随时间变化。而如果是频率风控引起的,表现通常是"跑一阵子才开始 403"。两种症状指向不同的原因,排查时先看"是不是每次都失败",能帮你快速分流。
顺带一提,UA 只是众多反爬维度之一。做得更严的站点还会看 TLS 指纹、请求频率、行为模式。但对大多数"403 挡命令行工具"的场景来说,换个 UA 就已经够了。真遇到更严的,再考虑上更完整的浏览器指纹方案,不必一上来就把工具链搞得那么重。