第三方系统向 WordPress 推送或回调数据时使用本方案。
要解决什么问题
WordPress 内置角色(Administrator / Editor / Shop Manager / Subscriber)都不适合给第三方:
- Administrator 的 Application Password 等于整站 REST
- Shop Manager / Editor 能改商品、文章
- Subscriber 几乎没写权限;若接口只检查
is_user_logged_in(),小程序 JWT 用户也能打回调口
Application Password 没有 scope:认证后等于该用户的全部 current_user_can()。因此必须:
- 专用角色、尽量少的能力
- 业务接口自己用
permission_callback检查能力 - 每个第三方一个 WP 用户,密码可单独吊销
核心决策
| 项 | 选择 | 原因 |
|---|---|---|
| 认证 | WP Application Password + HTTP Basic | 核心自带,第三方易对接;与 JWT Bearer 不冲突 |
| 角色 | integration | 不复用内置角色 |
| 能力 | 仅复用核心 import | 管理员本就有 import,便于联调;不授予 read |
| 命名空间 | integration/v1 | 不占用 auth/v1、业务 BFF、wp/v2 |
| 权限检查 | is_user_logged_in() && current_user_can('import') | 禁止「只要登录就放行」 |
| wp-admin | 进不去(符合预期) | 核心进后台需要 read;不必再写 admin_init 拦截 |
不要把生产环境的 Application Password 配在管理员账号上。
架构
Third party
Authorization: Basic username:app_password
│
▼
WP Application Passwords → wp_set_current_user
│
▼
register_rest_route permission_callback
· 未登录 → 401
· 无 import → 403
│
├─ POST /uploads → 解压到 uploads/integration/{folder}
├─ POST /mini-program-routes → update_option('mini_program_routes')
└─ POST /{payments|users|products} → 写入 integration_events,do_action
JWT 中间件只处理 Authorization: Bearer,Basic 交给核心,二者可共存。
目录与职责
本仓库对应实现(Acorn + App\ 命名空间)。移植时类名可改,职责不要混。
| 职责 | 本仓库路径 |
|---|---|
| 角色 / 能力常量与幂等注册 | src/wp-bootstrap/app/Integration/Capabilities.php |
init 上 ensure() | src/wp-bootstrap/app/Hooks/IntegrationHooks.php |
REST 注册与 permission_callback | src/wp-bootstrap/app/Routes/Integration/IntegrationRoute.php |
| 入站事件落库 | src/wp-bootstrap/app/Services/IntegrationEventService.php |
| 事件表 Model | src/wp-bootstrap/app/Models/IntegrationEvent.php |
| topic / 同步状态枚举 | src/wp-bootstrap/app/Enums/IntegrationTopic.php、IntegrationEventStatus.php |
| 表结构 | src/wp-bootstrap/database/migrations/2026_09_18_020700_create_integration_events_table.php |
无 Acorn 时:Hook 用普通插件 add_action('init', …);表用 dbDelta 或手工 SQL;Service 照搬即可。
移植清单
1. 角色(每个请求只在缺 cap 时写库)
add_role('integration', 'Integration', ['import' => true]);
// 已存在则:仅当 capabilities['import'] !== true 时 add_cap('import')
不要在每次 init 无条件 add_cap():WP_Roles::add_cap() 会 update_option。
不要授予 read。WordPress 仪表盘 / 个人资料都要求 read,缺它时登录 wp-admin 会得到 Sorry, you are not allowed to access this page.,这是预期,不是漏做拦截。
不要在代码里维护「禁止列表」去剥能力。额外权限交给后台(如 Members)管理。注意:若有人在后台去掉 import,ensure() 仍会加回来,因为接口门禁依赖它。若有人给这个角色加上 read,就能进后台。
2. REST 门禁
所有 integration/v1 路由共用:
if (!is_user_logged_in()) {
return new WP_Error('rest_not_logged_in', 'Authentication required.', ['status' => 401]);
}
if (!current_user_can('import')) {
return new WP_Error('rest_forbidden', 'Sorry, you are not allowed to do that.', ['status' => 403]);
}
return true;
JSON 请求体:Content-Type: application/json 时只用 get_json_params()。PHP 里 json_decode('{}') 是 [],不要用「空数组则回落到 get_params()」,否则会把 REST 内部参数写进业务存储。
3. 入站事件表(可选,用于异步同步)
表名不要加 wp_ 前缀(若用 Acorn / $wpdb->prefix)。
| 列 | 说明 |
|---|---|
user_id | 推送方 WP 用户 |
topic | payments / users / products |
payload | JSON |
status | 0 未同步,1 已同步 |
retry | 默认 0,业务侧可限制 0–5 |
note | 同步日志 |
created_at / updated_at | 时间戳 |
接收成功返回 202 { eventId, topic },并 do_action('wp_integration_{topic}', $event, $payload)。第一期不在接收时写业务表。
运维:给第三方开账号
- 用管理员账号后台新建用户,角色选 Integration(不要 Administrator)
- 管理员打开该用户的编辑页 → Application Passwords → 创建(integration 用户自己进不了 wp-admin)
- 把 用户名 + 明文密码 交给对方(密码只显示一次)
- 吊销:管理员删除对应 Application Password,不必删用户
本地可用 WP-CLI:
wp user create payments-bot payments-bot@inbound.invalid --role=integration --user_pass="$(wp eval 'echo wp_generate_password(32, true, true);')"
wp user application-password create payments-bot "Payment Gateway" --porcelain
Application Password 在非 HTTPS 环境仅 WP_ENVIRONMENT_TYPE=local(或等价)时可用。
反代必须把 Authorization 传到 PHP。若站点曾用 HTTP Basic 保护整站,确认不会吞掉 REST 的 Basic 头。
安全边界
已做:
- 专用角色 + 仅
import,不用管理员账号给生产第三方 - 不授予
read,因此登录 wp-admin 会被核心拒绝(Sorry, you are not allowed to access this page.)
未做(有意从简):
- Application Password 不能按 path 授权。持有
import的登录用户可以调用所有只检查import/is_user_logged_in()的 REST,包括本命名空间全部接口 - 未把 integration 用户锁死在
integration/v1(曾实现过命名空间锁定,已去掉) - 未再写
admin_init拦截:缺read已经进不去后台。若有人用 Members 等给该角色加上read,就能进 wp-admin,且因有import能打开「工具 → 导入」 - 事件接口无 schema、无幂等、无 body 大小上限
若其他项目威胁模型更严,建议加回:
rest_pre_dispatch:有integration角色且无manage_options时,只允许integration/v1admin_init再拒绝该角色(双保险,防止被加上read)- 自造 cap 替代
import,避免一旦有人补上read就能用「工具 → 导入」
扩展
- 新推送类型:给
IntegrationTopic加 case,或单独register_rest_route - 真正写订单 / 用户 / 商品:监听
wp_integration_{topic},更新status/retry/note - 多第三方最小权限:继续「一家一个用户」;若要按接口拆权,再拆 cap,而不是再拆角色
本仓库注册点
// Application::registerInitHooks
$container->get(\App\Hooks\IntegrationHooks::class)->load();
// Application::registerRestApiHooks
$container->get(\App\Routes\Integration\IntegrationRoute::class)->register_routes();
其他项目把这两处接到自己的 bootstrap / plugins_loaded 即可。