用 Python 标准库 urllib.request 调 WordPress REST API 时,如果响应体只有一行 error code: 1010,而且响应头里能看到 server: cloudflare,这不是 WordPress 的问题,也不是应用密码配错了——是 Cloudflare 的浏览器完整性检查(Browser Integrity Check)在边缘按 User-Agent 把 Python-urllib/3.12 这个默认 UA 拦掉了 [1]。最直接的解决办法是在请求里显式带一个能标识自己程序的 User-Agent 头,比如 AiContentStudio/1.0,其他逻辑都不需要改。
之所以能很快锁到这里,是因为我手上有两个 WordPress 站,都是 WordPress 6.8 + 子比主题:前面套了 Cloudflare 的那个,同样的代码抛 403;没走 Cloudflare 的这个(就是本站,响应头是 server: nginx),同样的发文代码返回 200。问题直接被压到 Cloudflare 这一层。
直接给 urllib 加一个 User-Agent 头
出问题的请求大致是这样写的:用 urllib.request.Request 构造请求,把 username:application_password 做 Base64 之后塞进 Authorization 头,然后 urlopen。两段代码的差别就一句。
# 不行的写法:默认 UA = "Python-urllib/3.12"
import urllib.request, base64, json
auth = base64.b64encode(b"myuser:xxxx xxxx xxxx xxxx xxxx xxxx").decode()
req = urllib.request.Request(
"https://example.com/wp-json/wp/v2/posts",
data=json.dumps({"title": "hi", "status": "draft"}).encode(),
headers={
"Authorization": f"Basic {auth}",
"Content-Type": "application/json",
},
method="POST",
)
urllib.request.urlopen(req) # → HTTPError 403, body: "error code: 1010"
# 改一行就行:加一个标识自己程序的 User-Agent
req = urllib.request.Request(
"https://example.com/wp-json/wp/v2/posts",
data=json.dumps({"title": "hi", "status": "draft"}).encode(),
headers={
"Authorization": f"Basic {auth}",
"Content-Type": "application/json",
"User-Agent": "AiContentStudio/1.0", # ← 这一行
},
method="POST",
)
with urllib.request.urlopen(req) as resp:
body = json.loads(resp.read())
print(resp.status, body.get("id"))
UA 字符串写成自己程序的名字、版本号就行,不必伪装成 Mozilla/5.0 ... 这种浏览器 UA。原因有两个:一是没必要:下面的实测里,只有 Python-urllib 这个 UA 被拦,python-requests 和自定义的程序名都能通过;二是用浏览器 UA 调用自家 API,日后想在 Cloudflare 日志里区分请求来源反而更困难,容易把自动化流量和真实访客搅在一起。
报错长什么样,以及为什么日志里查不到
实际抛出的异常是 Python 标准库原始报错,body 只剩一行字符串:
urllib.error.HTTPError: HTTP Error 403: Forbidden
把 e.read().decode('utf-8') 拿出来:
error code: 1010
我的第一反应是 tail -f 源站 Nginx 的 access log,结果里面根本没有这条请求。这是第一个关键线索:响应头既然是 server: cloudflare,请求在 Cloudflare 边缘就被拒了,没有回源,Nginx 和 WordPress 自然看不到。想确认的话,可以用浏览器或 curl 访问同一个站的任意页面做对照,那一条会出现在 access log 里,Python 发的那条不会。
同一个应用密码,另一个站一切正常
把代码原封不动换到本站(没套 Cloudflare,响应头是 server: nginx),用同一对应用密码、同一段 urllib.request 代码、同一台机器跑,发文章接口返回 200,分类接口也 200。也就是说,WordPress 6.8 和「子比主题」对 REST API 的常规鉴权和响应都没问题,应用密码的权限也是对的。表现差异只在「这个站前面挂没挂 Cloudflare」。
到这里我已经不怀疑 WordPress 这一侧了,问题被压缩到「Cloudflare 边缘是不是对特定 UA 做了拦截」。
同一 URL,五次请求,唯一变量是 UA
为了把变量钉死,我在套了 Cloudflare 的那个站上固定 /wp-json/wp/v2/categories?per_page=1 这个不写文章的只读接口(避免污染线上内容),同一台机器、同一时间,分别用五种 UA 打:
| 客户端 | UA | 返回 |
|---|---|---|
Python urllib.request 不带 headers |
Python-urllib/3.12(默认) |
403,error code: 1010 |
curl -A "Python-urllib/3.12" |
Python-urllib/3.12 |
403,error code: 1010 |
curl 不带 -A |
curl 默认 UA | 200 |
curl -A "python-requests/2.32.3" |
python-requests/2.32.3 |
200 |
urllib.request 加 headers={"User-Agent": "AiContentStudio/1.0"} |
AiContentStudio/1.0 |
200 |
五次结果对照下来,请求路径、Headers 里其他字段、请求方法都没变,唯一变量就是 UA,所以「Cloudflare 在按 UA 拦截 Python-urllib/3.12」就是那个原因,没有其他更合理的解释。需要特别注意的是第二行和第一行的对称:把 Python-urllib/3.12 这个 UA 字符串单独塞给 curl 也照样 1010,证明这是 UA 字符串本身的命中,跟 Python、跟 urllib 库无关。
Cloudflare 官方对 1010 的解释是:站点所有者根据浏览器签名禁止了访问 [1]。具体干活的是浏览器完整性检查,它默认开启,会拦下请求头像常见爬虫、脚本客户端的请求 [2],Python-urllib 正好命中。python-requests/2.32.3 这次没被拦,所以「我用 requests 就没事」的说法在这个场景下成立,但这只是当前规则下的结果,不保证以后也一样。
如果是自己的站,Cloudflare 侧可以怎么处理
代码层加 UA 已经够用,但站是自己的话,往往也希望把这一类「自己程序 → 自己接口」的调用在边缘层面也放开,避免将来其他同事或子项目踩同一个坑。两条可选思路:
比较稳妥的是在 WAF 自定义规则里加一条:URI 路径以 /wp-json/ 开头时,动作选 Skip,只勾选跳过 Browser Integrity Check [3]。这样其他防护照常生效,只是这个路径不再按 UA 签名拦截。另一种做法是直接在全站关掉浏览器完整性检查,影响范围大得多,不建议为了一个接口这么做。
副作用也要想清楚:跳过之后,任何脚本用任何 UA 都能打到 /wp-json/,接口本身的鉴权(应用密码)和速率限制就更重要了。调用方如果只有自己的程序,在客户端加 UA 已经足够,没必要动 Cloudflare。
FAQ
Q1:我用 python-requests 调同一个接口,会遇到 1010 吗?
实测下来没遇到。python-requests/2.32.3 这个默认 UA 不在这次 1010 触发的名单里,所以同样的 URL、同一对应用密码用 requests 默认调用可以直接返回 200。不过这只是当前规则下的结果,长期看还是主动设一个自己程序名的 UA 更稳。
Q2:这是 WordPress 应用密码权限配错了吗?
不是,两条直接证据。第一条:e.read() 出来的是裸字符串 error code: 1010,而 WordPress 原生的 401/403 错误响应是带 code、message 字段的 JSON 体,格式对不上;而且响应头是 server: cloudflare,源站 Nginx 的 access log 里也查不到这条请求——请求在 Cloudflare 边缘就被拦了,根本没到 WordPress。第二条:同一对应用密码在另一个不走 Cloudflare 的站是 200。应用密码配错或权限不够,返回的是 401 或带 WordPress 标准错误体的 403,不会是孤零零一行 error code: 1010。


暂无评论内容