每个请求都返回 407 Proxy Authentication Required,没有任何内容到达目标站点,而面板中的凭证看起来是正确的。这是这类产品中最常见的支持工单之一,也是解决速度最快的问题之一,因为能够导致 407 的原因数量很少,而且可以按固定顺序逐一排除。
以下是这个状态码实际的含义、值得检查的原因、检查的顺序,以及即使凭证正确也会产生 407 的客户端特有错误。
407 究竟是什么
407 来自代理,而不是来自你要访问的站点。这是代理表示请求到达时没有可接受的凭证的方式,是代理层等同于源服务器返回 401 的情况。这个区别对调试很重要:407 意味着你的流量已经到达了网关,而网关拒绝了它。连接没有问题,认证有问题。
这也意味着目标站点完全没有参与。如果你收到 407,无论你如何更改请求头、user agent、渲染方式或请求节奏都无济于事,因为你的请求根本没有离开代理。
首先值得检查的三个原因
在一个通过用户名表达定向参数的网关上,几乎每一个 407 都属于以下三种情况之一。
凭证错误。 拼写错误、从面板复制时带入的多余空白字符,或者来自其他产品的凭证。代理凭证通常与你的账户登录凭证不同,这是一个出乎意料常见的混淆点。
扩展用户名中的某个标志格式错误。 这是人们最容易忽略的原因,也是那些将定向参数编码在用户名中的网关所特有的问题。如果你的用户名带有国家、城市、会话或 TTL 标志,一个无法识别的值会使整个用户名无法解析,网关会将其当作认证失败而非定向错误来拒绝。拼写错误的国家代码、格式错误的城市 slug、含有非法字符的会话标识符,或者没有配套 sid 的 ttl,都属于这种情况。凭证本身完全正确,但用户名字符串不对。
密码已被轮换。 如果有人在面板中重新生成了密码,那么每个仍持有旧值的客户端都会返回 407,直到更新为止。这就是那种在一台机器上可以正常工作、在另一台机器上却失败的典型情况。
排查顺序
按照以下列表逐项排查,一旦成功就停止,因为解决问题的那一步就能确定原因。
第一步:去掉所有标志,测试裸凭证。 这一步能区分是凭证问题还是标志问题,应该永远放在第一位。
# bare username, no targeting flags at all
curl -x customer-USERNAME:PASSWORD@p.shifter.io:443 https://ipinfo.io/json
如果这样成功了,说明你的凭证没有问题,问题出在标志上。如果仍然返回 407,那么凭证本身就是错误的,无论怎么修复标志都无济于事。
第二步:逐个添加标志。 先添加国家,然后是城市或 ASN,最后是会话和 TTL。重新引发 407 的那个标志就是格式错误的那个,你现在就能准确知道该检查哪个值。
curl -x customer-USERNAME-country-us:PASSWORD@p.shifter.io:443 https://ipinfo.io/json
curl -x customer-USERNAME-country-us-city-new_york:PASSWORD@p.shifter.io:443 https://ipinfo.io/json
curl -x customer-USERNAME-country-us-city-new_york-sid-abc123:PASSWORD@p.shifter.io:443 https://ipinfo.io/json
注意格式:国家代码是两字母的 ISO 代码,所以 uk 是一个常见的错误,正确的应该是 gb。城市 slug 使用下划线而不是空格或连字符,例如 new_york。而 ttl 只有在配合 sid 使用时才有效,单独使用 TTL 是无效的。完整语法请参见网关与认证文档,定向选项在城市级定向和ASN 定向中有详细说明。
第三步:从面板重新复制密码。 如果第一步的裸凭证测试就失败了,请直接从面板中获取密码,而不是从你的笔记或配置文件中获取,并特别检查是否有尾随空格、来自文档的智能引号,或者截断的粘贴内容。
第四步:在所有地方推送更新后的值。 如果本地可以工作但生产环境不行,说明在环境变量、密钥管理器、容器镜像或 CI 配置中存在过时的副本。这是部署问题,而不是代理问题。
看起来像 407 但其实不是的错误
区分这些错误可以节省大量精力,因为每种错误的解决方法都不同。
在这个网关上,502 表示当时没有地址与你的过滤条件匹配。这是一个定向问题,而不是认证问题:你的凭证已被接受,但对于你请求的组合当时没有可用资源。可以放宽过滤条件、从城市级降到国家级,或者放松一个严格匹配的标志。
509 表示套餐带宽已耗尽且未启用超额。认证是成功的,只是配额用完了。相关的容量规划建议参见估算每月带宽。
Connection refused 表示你根本没有到达网关,通常是因为你指向的是基于端口的旧版主机,而非当前端点,或者是因为你的网络出口被阻断了。在假设是认证问题之前,先用 nc -vz p.shifter.io 443 测试原始连接性。
来自目标站点的 403 或质询页面 意味着认证完全成功,只是站点拒绝了你,这是一个完全不同的问题,在避免被封锁中有讨论。
导致 407 的客户端特有错误
有时候凭证和标志都是正确的,问题出在客户端本身。
密码中含有特殊字符。 如果密码中包含在 URL 中有特殊含义的字符,比如 @、:、#、/ 或 %,直接将其嵌入代理 URL 会破坏解析过程,凭证到达时会被打乱。请对其进行百分号编码。
import requests
from urllib.parse import quote
user = "customer-USERNAME-country-us"
pwd = quote("p@ss:word/123", safe="") # encode before embedding
PROXY = f"http://{user}:{pwd}@p.shifter.io:443"
r = requests.get("https://ipinfo.io/json",
proxies={"http": PROXY, "https": PROXY}, timeout=15)
print(r.status_code, r.text[:120])
混淆了代理认证与目标站点认证。 curl -U 设置的是代理凭证;-u 设置的是目标站点的凭证。将代理凭证作为 Authorization 请求头发送是没有用的,因为代理认证是通过 Proxy-Authorization 传递的,而大多数客户端在凭证包含在代理 URL 中时会自动为你设置这一项。
HTTPS 下凭证丢失。 某些客户端配置只为 HTTP 设置了代理,因此普通请求可以认证成功,而 HTTPS 请求则不能。请同时设置两项,如上面的 Python 示例所示。
并非你以为的那样的环境变量。 在 shell 配置文件、Dockerfile 或 CI 运行器中设置的 HTTP_PROXY 和 HTTPS_PROXY 可能会悄悄覆盖你代码中传入的值,导致应用程序针对的代理字符串与你源代码中的完全不同。调试时应打印出实际生效的代理配置,而不是相信代码本身。
某个库不发送预先的代理认证。 有些 HTTP 客户端会等待被质询后才发送凭证,并且在处理质询时出错,尤其是在某些隧道配置下。如果裸 curl 可以工作而你的应用程序不行,差异就出在客户端上,而不是网关上。常见技术栈的可用配置参见在 Python 中使用住宅代理。
在生产代码中处理 407
有一点需要注意:407 是一个终止性错误,而不是暂时性错误。重试它毫无意义,因为下一次尝试时凭证仍然会是错误的,针对认证失败进行重试循环只会浪费带宽,而且从外部看起来可能像是撞库攻击的模式。应将其归类为终止性错误,明确报错并发出警报,这是重试与退避中提到的分类原则。
轮换凭证需要一个部署计划,而不是在面板中点一下就完事,因为每个持有旧值的客户端都会在轮换的那一刻开始失败。应先更新密钥存储,再推送到各客户端,然后再进行轮换。
总结
407 意味着你的请求已经到达网关,而网关拒绝了凭证,因此目标站点与此无关,连接性也已得到证实。首先测试不带任何标志的裸凭证,因为这一步能把问题一分为二,然后逐个添加标志找出格式错误的那一个,记住错误的国家代码或没有会话的 TTL 会使整个用户名无法解析。如果裸凭证测试失败,就从面板重新复制密码,如果在一处能用而在另一处不能用,就检查环境变量和密钥存储中是否有过时的值。排除那些看起来相似的情况:502 是过滤条件没有匹配结果,509 是带宽问题,connection refused 是主机地址错误,而站点返回的 403 根本不是认证问题。而且,永远不要重试 407。