后台菜单在测试环境明明正常,上线后普通用户却能看到"系统设置":我漏掉的 `add_menu_page` position 参数与权限回调的"静默失效"机制
上周把一个新插件丢到预发布环境,运营在群里甩了张截图:一个只配了 `editor` 角色的测试账号,左侧菜单里赫然躺着"数据迁移"和"日志审计"。我本地用管理员账号测了八百遍,完全没复现。
最后定位到两个坑,都是 add_menu_page 的"静默宽容"惹的祸——它不会报错,只是把你漏掉的东西默默补成默认值,而默认值往往是 manage_options。
坑一:position 参数被"挤占"后的权限继承
先看这段看似无害的代码:
add_menu_page(
'数据迁移',
'数据迁移',
'manage_options', // ← 权限
'my-migration',
'my_migration_page',
'dashicons-migrate',
3 // ← position
);
问题出在 position = 3。WordPress 后台菜单的 position 不是"排序权重",是绝对坐标占位。3 这个位置默认是 edit.php(文章菜单)。如果另一个插件或主题也注册了 3,或者你多次启用时位置冲突,WordPress 不会报错,而是按注册时序后覆盖前。
更隐蔽的是:如果覆盖发生时权限参数不一致,子菜单的权限检测会向上继承最后被写入的那个父级权限。我本地只有一个插件,线上有另一个运营插件也占用了 3,结果它的 read 权限把我的 manage_options 顶掉了,菜单变成所有人可见。
现在我的 position 策略改成带小数偏移,避开整数雷区:
// 59.3 = 设置菜单(60) 前面一点,又避开 59 的插件冲突
add_menu_page( ..., 59.3 );
// 或者用字符串前缀确保唯一性,虽然文档没写但实测有效
add_menu_page( ..., '59.3_my_plugin' );
坑二:权限回调的"假阴性"与 capability 映射
另一个菜单我写了自定义 capability:
add_menu_page(
'日志审计',
'日志审计',
'view_audit_log', // ← 自定义 capability
'my-audit',
'my_audit_page'
);
然后我在 admin_init 里用 current_user_can('view_audit_log') 做页面内二次校验。本地管理员一切正常,线上编辑器账号——菜单没了,但直接访问 /wp-admin/admin.php?page=my-audit 居然能进。
原因是:WordPress 的菜单权限检测只在渲染菜单时执行一次,页面本身的访问并不再次校验 add_menu_page 里的 capability。我的自定义 capability 没有通过 map_meta_cap 映射到实际权限,导致 current_user_can 返回 true(未映射的 cap 默认对所有人 true,这是历史遗留行为)。
修正方案,必须挂 map_meta_cap:
add_filter( 'map_meta_cap', function( $caps, $cap, $user_id, $args ) {
if ( 'view_audit_log' === $cap ) {
$caps = [ 'manage_options' ]; // 实际映射到管理员
}
return $caps;
}, 10, 4 );
我的上线前 Checklist(菜单/权限专项)
从这次事件后,我拆了一张单独的菜单权限检查表,不跟通用的混在一起:
注册阶段
- [ ] 所有
add_menu_page/add_submenu_page的 position 用xx.y或字符串格式,避开整数冲突 - [ ] 自定义 capability 必须配套
map_meta_cap过滤器,禁止裸奔 - [ ] 子菜单的
slug与父菜单做前缀关联检查,防止menu_page_url同名吞并
校验阶段
- [ ] 用
wp_get_current_user()切换到subscriber/editor/author三种角色分别扫菜单 - [ ] 直接构造 URL 访问
admin.php?page=xxx,绕开菜单渲染做权限穿透测试 - [ ] 检查
$_REQUEST['page']与当前 capability 的匹配,防止 A 菜单配 B 权限的"交叉污染"
配置阶段
- [ ] 如果菜单显示依赖插件设置项,确认
get_option的默认值在首次激活时已写入,防止false导致的意外分支 - [ ] 多站点环境额外测一次
switch_to_blog后的菜单隔离性
一个快速自检的 snippet
丢进临时 mu-plugin,访问任意后台页看输出:
add_action( 'admin_menu', function() {
global $menu;
$current_user = wp_get_current_user();
error_log( "User: {$current_user->roles[0]}, Menus: " . json_encode( array_column( $menu, 2 ) ) );
}, 999 );
看日志里角色与菜单 slug 的对应关系,比肉眼扫界面靠谱得多。
你们有遇到过菜单"时有时无"、或者权限在本地线上表现不一致的情况吗?我怀疑还有 $_wp_submenu_nopriv 这个内部变量的坑没踩完。