本文约 6,742 字,预计阅读 17 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:为什么现在必须懂 REST API
早期插件要拿数据只能在 PHP 里写 WP_Query。现在这些场景全走 REST API:
- 用 Python/Node 脚本批量发布文章(本项目阶段 3 就要用);
- 小程序、App 拉取站点内容;
- 前后端分离(Next.js / Nuxt 做前台,WordPress 只做后端);
- 区块编辑器本身——Gutenberg 完全依赖 REST API 读写数据。
反过来它也是攻击面:未授权的用户枚举、内容抓取。所以既要会用,也要知道怎么收口。
原理分析:wp-json 的结构与默认端点
所有 REST 请求走同一个前缀:
https://example.com/wp-json/
根路径返回 API 索引(所有已注册的命名空间和路由):
curl -s https://example.com/wp-json/ | python -m json.tool | head -40
核心命名空间是 wp/v2:
| 端点 | 说明 |
|---|---|
/wp-json/wp/v2/posts |
文章列表,支持 ?per_page=&page=&search=&categories= |
/wp-json/wp/v2/posts/<id> |
单篇文章 |
/wp-json/wp/v2/pages |
页面 |
/wp-json/wp/v2/categories / tags |
分类 / 标签 |
/wp-json/wp/v2/media |
媒体库 |
/wp-json/wp/v2/users |
用户(需鉴权) |
/wp-json/wp/v2/settings |
站点设置(需鉴权) |
只读请求默认不需要鉴权(公开内容本来就能访问)。写操作(POST/PUT/DELETE)必须鉴权。
批量拉取时看分页响应头:
curl -I "https://example.com/wp-json/wp/v2/posts?per_page=100"
# X-WP-Total: 109
# X-WP-TotalPages: 2
保姆级实操
一、只读调用(无需鉴权)
# 最新 10 篇
curl -s "https://example.com/wp-json/wp/v2/posts?per_page=10"
# 只要标题和链接
curl -s "https://example.com/wp-json/wp/v2/posts?_fields=id,title,link&per_page=100"
# 搜索
curl -s "https://example.com/wp-json/wp/v2/posts?search=缓存"
# 按分类 / 日期 / 排序
curl -s "https://example.com/wp-json/wp/v2/posts?categories=5"
curl -s "https://example.com/wp-json/wp/v2/posts?after=2026-01-01T00:00:00&before=2026-10-01T00:00:00"
curl -s "https://example.com/wp-json/wp/v2/posts?orderby=title&order=asc"
二、鉴权方式一:应用密码(推荐)
WordPress 5.6+ 内置。生成:后台 用户 → 个人资料 → 应用密码 → 输入名称 → 生成,得到 xxxx xxxx xxxx xxxx xxxx xxxx。
curl -X POST "https://example.com/wp-json/wp/v2/posts" \
-u "UFO_PF:xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "测试文章",
"content": "正文内容",
"status": "draft",
"categories": [5],
"tags": [12, 13]
}'
Python 写法:
import requests, base64
BASE = "https://example.com/wp-json/wp/v2"
USER = "UFO_PF"
APP_PW = "xxxxxxxxxxxxxxxxxxxxxxxx"
token = base64.b64encode(f"{USER}:{APP_PW}".encode()).decode()
headers = {
"Authorization": f"Basic {token}",
"Content-Type": "application/json",
}
r = requests.post(f"{BASE}/posts", headers=headers, json={
"title": "REST API 发布的文章",
"content": "<p>正文</p>",
"status": "publish",
"excerpt": "摘要",
})
print(r.status_code, r.json().get("link"))
注意:应用密码要求站点必须启用 HTTPS,否则 WordPress 会拒绝。生产环境请务必上 HTTPS。
三、鉴权方式二:Cookie + nonce(浏览器内、主题插件里)
wp_localize_script( 'my-script', 'wpApiSettings', array(
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
) );
fetch(`${wpApiSettings.root}wp/v2/posts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': wpApiSettings.nonce,
},
body: JSON.stringify({ title: '新文章', status: 'draft' }),
})
.then(r => r.json())
.then(d => console.log(d));
四、鉴权方式三:JWT(前后端分离、App)
装插件 “JWT Authentication for WP REST API”:
// wp-config.php
define( 'JWT_AUTH_SECRET_KEY', '你的随机长字符串' );
define( 'JWT_AUTH_CORS_ENABLE', true );
Nginx 需要放行 Authorization 头:
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
获取 token:
curl -X POST "https://example.com/wp-json/jwt-auth/v1/token" \
-H "Content-Type: application/json" \
-d '{"username":"UFO_PF","password":"真实登录密码"}'
之后带 Authorization: Bearer <token> 请求。
五、自定义端点(register_rest_route)
add_action( 'rest_api_init', function() {
register_rest_route( 'mmf/v1', '/stats', array(
'methods' => 'GET',
'callback' => 'mmf_api_stats',
'permission_callback' => '__return_true',
) );
register_rest_route( 'mmf/v1', '/post-count', array(
'methods' => 'GET',
'callback' => 'mmf_api_post_count',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
'args' => array(
'type' => array(
'default' => 'post',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => function( $param ) {
return in_array( $param, array( 'post', 'page' ), true );
},
),
),
) );
});
function mmf_api_stats( $request ) {
return new WP_REST_Response( array(
'posts' => (int) wp_count_posts( 'post' )->publish,
'comments' => (int) wp_count_comments()->approved,
'time' => current_time( 'mysql' ),
), 200 );
}
function mmf_api_post_count( $request ) {
$type = $request->get_param( 'type' );
$c = wp_count_posts( $type );
return rest_ensure_response( array( 'type' => $type, 'publish' => (int) $c->publish ) );
}
permission_callback 在 WordPress 5.5+ 是必填的,不写会在站点健康里报”REST API 端点缺少权限回调”。
六、安全收口
关闭用户端点(防用户名枚举):
add_filter( 'rest_endpoints', function( $endpoints ) {
if ( isset( $endpoints['/wp/v2/users'] ) ) {
unset( $endpoints['/wp/v2/users'] );
}
return $endpoints;
});
限制匿名写操作:
add_filter( 'rest_authentication_errors', function( $result ) {
if ( ! empty( $result ) ) {
return $result;
}
if ( ! is_user_logged_in() && in_array( $_SERVER['REQUEST_METHOD'], array( 'POST', 'PUT', 'DELETE' ), true ) ) {
return new WP_Error( 'rest_not_logged_in', '请先登录', array( 'status' => 401 ) );
}
return $result;
});
不要一刀切关闭 REST API,否则区块编辑器、站点编辑器和很多插件会失效。只关闭不需要的端点。
常见踩坑与失败案例
- 401
rest_cannot_create:应用密码权限不够,或该用户角色没有publish_posts能力。检查wp cap list <role>。 - 案例一:
curl用应用密码一直 401,最后发现站点在 Cloudflare 后面开了”机器人攻击防护”,拦截了带 Basic Auth 的自动化请求。在 WAF 里放行/wp-json/wp/v2/posts的 POST。 - 403
rest_forbidden:安全插件(Wordfence)拦了,去插件日志看。 - 案例二:Nginx 反代没传
Authorization头,JWT 一直拿不到 token。加fastcgi_param HTTP_AUTHORIZATION $http_authorization;。 - 参数校验失败:
args里required => true但没传,会返回rest_missing_callback_param。 - HTTPS 要求:应用密码在 HTTP 站点上被禁用。开发环境可临时加:
php
add_filter( 'wp_is_application_passwords_available', '__return_true' );
- 返回 404:固定链接是朴素模式时 REST 路由不工作。去 设置 → 固定链接 选一个非朴素结构。
- 性能:
per_page最大 100,批量拉取要分页;用_fields裁剪响应能显著减小体积。
总结
REST API 三个必知点:只读默认公开、写操作必须鉴权、生产环境首选”应用密码 + HTTPS + Basic Auth”(比 JWT 简单、比 Cookie nonce 更适合脚本)。自定义端点用 register_rest_route,permission_callback 是必填项。安全上只做减法——关掉 /wp/v2/users 防用户枚举,必要时限制匿名写,绝不整体关闭 REST API。批量脚本用 X-WP-Total 头读总数做分页,用 _fields 裁剪响应。
