上周我想写个脚本把小说章节批量发布到博客,第一道坎就是认证:怎么让脚本以「我」的身份登录?WordPress REST API 官方支持好几种认证方式,但在我的站点(WordPress 7.1 + OpenResty + PHP 8.3,纯 HTTP 无 HTTPS)上实测,只有一种走得通。这篇文章记录完整过程、对比和最终方案。
一、Basic Auth:看起来最省事,实测 401
最直观的方式:把用户名密码放进 HTTP 的 Authorization 头。
curl -u 用户名:密码 \
http://你的站点/wp-json/wp/v2/users/me
很多教程说这种方式在非 SSL 站点可用,于是我先试了它。结果是 401:
{
"code": "rest_not_logged_in",
"message": "您目前没有登录。",
"data": { "status": 401 }
}
我排除了「密码里特殊字符」这个变量——手工把 Base64 头拼出来再发一次,还是 401。这个错误是 WordPress REST 的标准错误结构,说明请求确实到了 WordPress 核心,只是认证没被接受。结论:我的站点当前环境不接受这种认证方式,可能是宿主反向代理或安全策略拦了 Authorization 头,也可能是这套核心/插件组合没有启用它。原因不重要,重要的是生产流程不要依赖它。
二、Application Password:官方推荐,但要求 HTTPS
这是 WordPress 团队最希望你用的方式:
- 后台 → 用户 → 个人资料
- 拉到最下面「应用程序密码」一节
- 输入一个名字(比如
publish-script),点创建,会得到一串xxxx xxxx xxxx xxxx xxxx xxxx格式的密码
用法和 Basic Auth 一样,只是密码换成它:
curl -u 用户名:xxxx-xxxx-xxxx-xxxx-xxxx \
https://你的站点/wp-json/wp/v2/posts
好处很实在:它是独立凭证,可随时吊销,不会暴露你的登录密码,官方文档也把它作为首选。但有一个硬条件:只在 HTTPS 下生效。我的博客目前是纯 IP + HTTP 访问,建了也用不了。所以这一条被我记进了待办:等域名和证书下来,认证整体迁过去。
三、Cookie + X-WP-Nonce:最终采用的方案
第三种方式的原理和浏览器一致:先用登录表单拿到会话 Cookie,之后每次 REST 请求带上 Cookie 加一个时效性 nonce。完整流程四步:
GET /wp-login.php—— 这一步最容易被忽略。WordPress 在这个页面会种一个 test cookie,跳过它直接 POST,会收到「Cookies 被阻止或者您的浏览器不支持」POST /wp-login.php(提交 log、pwd、testcookie)——成功后拿到wordpress_logged_in_*会话 CookieGET /wp-admin/post-new.php(任意加载了区块编辑器的后台页面)——页面源码里有一个wpApiSettingsJS 对象,其中的"nonce"字段就是需要的X-WP-Nonce- 每次 REST 请求带上 Cookie +
X-WP-Nonce头
PowerShell 核心实现(.NET HttpClient 会自动管理 Cookie 容器):
$handler = New-Object System.Net.Http.HttpClientHandler
$client = New-Object System.Net.Http.HttpClient($handler)
# 1) 先 GET 登录页(拿 test cookie)
$null = $client.GetAsync('http://site/wp-login.php').GetAwaiter().GetResult()
# 2) POST 登录
$body = 'log=user&pwd=' + [uri]::EscapeDataString($pass) + '&testcookie=1'
$content = New-Object System.Net.Http.StringContent(
$body, [System.Text.Encoding]::UTF8, 'application/x-www-form-urlencoded')
$null = $client.PostAsync('http://site/wp-login.php', $content).GetAwaiter().GetResult()
# 3) 从后台页面取 nonce
$admin = $client.GetAsync('http://site/wp-admin/post-new.php').GetAwaiter().GetResult()
$html = $admin.Content.ReadAsStringAsync().GetAwaiter().GetResult()
$nonce = ([regex]::Match($html, '"nonce"\s*:\s*"([a-f0-9]+)"')).Groups[1].Value
# 4) 调 API
$req = [System.Net.Http.HttpRequestMessage]::new('GET', 'http://site/wp-json/wp/v2/users/me')
$req.Headers.Add('X-WP-Nonce', $nonce)
$me = $client.SendAsync($req).GetAwaiter().GetResult()
两个注意点:
- nonce 有时效窗口。默认 tick 是 15 分钟,一个 nonce 在「当前 + 上一」两个窗口内(约 30 分钟)可重复使用。批量任务很快跑完,一次 nonce 够用;长任务要在过期前重新取一次
- 如果用 PowerShell 的
Invoke-WebRequest -WebSession,注意 Cookie 集合解析可能不稳定(我实测拿到过名字、值全空的 Cookie)。换成 .NET HttpClient 自带的 Cookie 容器更稳
四、三种方式对比
| 方式 | 需要 HTTPS | 脚本友好度 | 凭证生命周期 | 在我站的结论 |
|---|---|---|---|---|
| Application Password | 是 | 高 | 可长期,随时吊销 | 首选(等证书) |
| Cookie + Nonce | 否 | 中 | nonce 约 30 分钟,cookie 按设置 | 当前在用 |
| Basic Auth | 理论不需要 | 高 | — | 实测 401,不可用 |
五、结论
- 站点有 HTTPS:直接 Application Password。凭证可吊销、不暴露登录密码,官方推荐
- 像我这样纯 HTTP、本机自动化:Cookie + Nonce 可行。本质是把浏览器登录过程脚本化
- Basic Auth:当调试时的快捷手段用,别在正式流程里赌它
完整的发布脚本在下一篇(开发实战分类)讲:内容清洗、幂等设计,以及路上踩的坑。
