curl 返回 403,但浏览器正常工作:找出缺失的部分
浏览器加载成功,而 curl 返回 403。这两种请求之间的差距总是有限且可查的——以下是如何在五分钟内将其分解。
你将一个 URL 粘贴到 Chrome 中,页面加载成功。你将相同的 URL 粘贴到 curl 中,却得到 403 Forbidden。在这两秒之间,资源没有变化,因此差异完全在于请求——而请求是一个有限且可检查的东西。本指南是一个二分程序:精确重放浏览器发送的内容,然后逐步删除部分,直到 403 返回。你最后删除的部分就是答案。嫌疑项按通常的顺序排列:User-Agent、Referer、cookies、TLS 指纹和 JavaScript。
步骤 0:证明请求确实不同
在进行理论化之前,查看 curl 实际发送的内容。使用 -v 你可以看到请求行、每个头和 TLS 握手。默认的 curl 请求非常简陋——通常只有 Host、User-Agent: curl/8.x 和 Accept: */*。而浏览器会发送十多个。
# what you send, what you get back, and the TLS details
curl -v -o /dev/null https://target.example/page
# just the response headers, quickly
curl -sS -o /dev/null -D - https://target.example/page
像仔细阅读状态行一样仔细阅读响应头。在最常见的情况下,其中一个可以直接解决问题:Vary: User-Agent 意味着服务器故意根据你声称的身份提供不同的响应。在一个文档详尽的 Stack Overflow 案例中,curl -f 对一个普通的 Apache 2.4.38 主机返回 403,而 wget 则以 200 获取相同的文件——成功的响应正好带有 Vary: User-Agent 头。将 -A 'Wget/1.21.2' 传递给 curl 立即解决了问题。站点所有者在滥用后将 curl 的用户代理列入黑名单;请求的其他部分无关紧要。
在你阅读输出时:curl: (22) The requested URL returned error: 403 不是一个单独的问题。退出代码 22 是 -f/--fail 对任何 HTTP 错误的处理——该标志抑制正文并使命令失败。暂时删除 -f 以便你可以实际阅读阻止页面,通常会指出阻止你的系统。
步骤 1:复制为 cURL,30 秒答案
两个主要浏览器都可以提供它们刚刚发出的确切请求。打开 DevTools,进入网络选项卡,右键单击请求并选择“复制为 cURL”。Chrome 自版本 26 起提供此功能,Firefox 自版本 31 起提供,输出包括每个头、每个 cookie 和 referer。将其粘贴到终端中:如果返回 200,问题肯定出在请求形状上,步骤 2 找出哪一部分。
这里有一个陷阱会浪费很多时间。如果 URL 重定向,网络面板会在导航时清除,你会复制错误的请求。首先在 Chrome 中勾选“保留日志”或在 Firefox 中勾选“持久日志”,这样你就可以看到重定向的请求和最终提供内容的请求。重定向链很重要:在一个著名的 Unix Stack Exchange 线程中,服务器检查 Referer,然后通过 302 跳转到一个不检查任何内容的位置——这使得失败看起来是随机的,直到整个链可见。

步骤 2:分解头
从工作中的“复制为 cURL”命令开始,一次删除一个头,每次删除后重新运行。第一个导致 403 返回的删除就是你的罪魁祸首。实际上,它几乎总是四个之一。
curl -sS -o /dev/null -w '%{http_code}\n' \
-A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
-H 'Accept-Language: en-GB,en;q=0.9' \
-e 'https://target.example/' \
-b 'session=abc123; consent=1' \
-L \
'https://target.example/page'
- User-Agent (
-A) — 直接被阻止,或者服务器根据它进行分支。测试当前的 Chrome 字符串,然后是 wget 的,再然后是一个无意义的;结果模式告诉你是否遇到了黑名单或白名单。 - Referer (
-e) — 资产和下载链接通常返回 403,除非请求看起来像是来自站点自己的页面。根据规范,这个头是可选的,这正是人们忘记它的原因。 - Cookies (
-b) — 在早期页面设置的同意、会话或反机器人 cookie。几秒钟内确认:在隐私窗口中打开 URL。如果浏览器在此处也返回 403,cookies 就是答案。 - Authorization (
-u,或一个 bearer 头) — 签名或令牌化的 URL 在上下文外复制时经常返回 403,因为令牌绑定到会话或已过期。
此步骤的两个结束细节。引用 URL:否则,包含 & 或访问令牌的查询字符串会被 shell 搅乱,导致的 403 与服务器无关。如果你不是从 shell 而是从 PHP 或 Node 进行调试,请在那里复制相同的头集——PHP 内的 libcurl 默认值与命令行工具的不同,这就是为什么相同的请求可以在终端中通过而在代码中失败。我们的 curl 代理示例完整介绍了标志语法。
步骤 3:当相同的头仍然返回 403
如果浏览器头的逐字节副本仍然失败,那么决定是在解析头之前做出的。它们下面有两层。
TLS 指纹。 你的 ClientHello——密码套件、扩展、曲线偏好、ALPN 以及随后的 HTTP/2 设置帧——哈希为一个 JA3 或 JA4 值。基于 OpenSSL 构建的 curl 产生的结果没有任何浏览器会产生,而反机器人系统会根据你声称的 User-Agent 进行比较。声称是 Chrome 而握手像 OpenSSL 是一个他们专门捕捉的矛盾。解决方法是一个能重现浏览器握手的客户端:命令行中的 curl-impersonate 或 Python 中的 curl_cffi。
# pip install curl_cffi
from curl_cffi import requests
proxy = "http://USER:PASS@gate.quantumproxies.io:8000"
r = requests.get(
"https://target.example/page",
impersonate="chrome", # browser ClientHello + HTTP/2 settings
proxies={"http": proxy, "https": proxy},
timeout=20,
)
print(r.status_code, r.headers.get("content-type"))
你的 IP。 工作的浏览器通常在你的家庭连接上,而 curl 则在 VPS 上运行。托管 ASN 是公开的且预先评分的,因此来自住宅地址的相同请求在被读取之前就被不同地判断了。更换出口是一个使用 住宅代理 的单行更改——90M+ IP 覆盖 200+ 国家,HTTP 和 SOCKS5 在每个计划中——这是排除网络最快的方法。指纹层的机制在 JA3/JA4 TLS 指纹识别中。

步骤 4:页面需要浏览器,而不是客户端
有时 403 并不是对你的判断——它是一个你从未尝试过的挑战的失败模式。关于链接检查器访问 npmjs.com 的公共 GitHub 讨论明确指出:curl 无法产生有效的挑战解决方案,因此请求被 403 阻止。服务器发布一个小的 JavaScript 问题,等待片刻以获取答案,并拒绝任何无法执行的请求。没有头集、没有指纹和没有 IP 能通过需要运行代码的测试。
在那时你有三个诚实的选择:驱动一个真实的浏览器并支付成本,找到页面本身调用的 JSON 端点(通常就在你已经打开的网络选项卡中),或将 URL 交给一个按需渲染的服务。QuantumProxies 的 Scraper API 就是最后一个选项——浏览器级 TLS,住宅出口,仅在页面需要时渲染 JavaScript,从一个请求中返回 markdown、JSON 或原始 HTML。如果你得到的是一个空页面而不是一个禁止页面,那是不同的诊断:请参阅 为什么你的爬虫返回空页面。如果阻止页面带有 Cloudflare Ray ID,请转到 Cloudflare 错误 1020。
常见问题解答
为什么 curl 得到 403 而我的浏览器没有?
因为 curl 发送的头大约有三个,没有 cookies,没有 referer,并且是非浏览器的 TLS 指纹,而你的浏览器发送十多个头,一个 cookie jar 和一个 Chrome 握手。服务器拒绝的是请求,而不是资源。用“复制为 cURL”重放浏览器的确切请求,然后逐个删除头以找出哪个差异重要。
为什么 wget 成功而 curl 得到 403?
几乎总是 User-Agent 的问题。一些服务器在滥用后专门将 curl 的 UA 列入黑名单,而 wget 的则不受影响——一个记录的案例显示了一个 Vary: User-Agent 响应头,确认服务器在此基础上进行分支,curl -A 'Wget/1.21.2' 恢复了 200。wget 还默认发送 Accept-Encoding 和 Connection,这有时也很重要。
如何在 curl 中设置 User-Agent?
使用 -A 'string',或等效的 -H 'User-Agent: string'。优选完整、当前的浏览器字符串,而不是截断的 Mozilla/5.0,因为一些服务器现在拒绝仅发送两个标记的请求。将其与匹配的 Accept 和 Accept-Language 值配对,以保持整个集合的一致性。
curl 错误 22 是什么意思?
退出代码 22 是由 -f/--fail 产生的,只要服务器返回 HTTP 错误,消息就会引用状态——通常是 403。这是一个报告标志,而不是一个独立的故障。删除 -f 以查看响应正文,这通常比退出代码更好地解释了阻止原因。
代理能修复 curl 403 吗?
它修复了由 IP 声誉或地理位置引起的子集——当你的脚本在云主机上运行而你的浏览器没有时,这是一个很大的子集。它不会修复缺失的 referer、缺失的 cookie 或 JavaScript 挑战。首先测试头,因为它们不花钱,然后更改出口 IP 以隔离网络层。
这里没有神秘,只有一个差距:浏览器发送了一个请求,而你发送了另一个。复制浏览器的请求,缩小它直到它出错,你总能找到重要的部分——通常是一个头,有时是一个指纹,偶尔是一个需要真实浏览器回答的挑战。