权限管理
ThinkAdmin 的后台权限由 RbacAccess 路由中间件、AdminService、NodeService、菜单服务和后台用户授权数据共同完成。实现上不是完整的“角色表”模型,而是“权限组 + 功能节点 + 用户授权”的简化 RBAC:
- 控制器注释由
NodeService::getMethods()扫描成节点。 - 权限组保存在
system_auth,权限组与节点关系保存在system_auth_node。 - 后台用户的
system_user.authorize保存已分配的权限组 ID。 - 用户登录后,
AdminService::apply(true)将可访问节点写入 Session 的user.nodes。 - 菜单显示和模板按钮显示再通过
AdminService::check()或全局函数auth()判断。
基础模型
功能节点
功能节点来自应用和插件控制器,格式通常为:
应用/控制器/方法示例:
admin/user/index
admin/user/add
admin/user/remove
plugin-wemall/shop.goods/index节点扫描规则来自 think\admin\service\NodeService:
- 扫描
app/下所有 PHP 文件中符合应用/controller/*.php结构的控制器,以及已安装插件目录下所有controller/*.php控制器。 - 跳过
config/app.php中rbac_ignore配置的应用。 - 跳过
think\admin\Controller基类中的公共方法。 - 控制器类名会转为小写节点,驼峰会转下划线;方法名只转小写,不会拆分驼峰。
- 多级控制器在节点中使用点号,例如
shop/goods/Cate对应shop.goods.cate。
权限组
权限组是后台“系统权限管理”里的授权分组,对应 system_auth。给权限组勾选节点后,关联数据写入 system_auth_node。
只有标注了 @auth true 的方法节点会进入权限树供勾选;控制器父节点会作为树形展示节点补齐。
用户授权
后台用户对应 system_user。用户表中的 authorize 字段保存权限组 ID 列表。
用户登录成功后,后台登录控制器会调用:
\think\admin\service\AdminService::apply(true);该方法会读取用户的权限组,筛选启用状态的 system_auth,再从 system_auth_node 读取节点列表写入 Session。
超级管理员
超级管理员不是固定用户 ID,而是账号名等于 config/app.php 中的 super_user 配置。默认值为 admin。
return [
'super_user' => 'admin',
];AdminService::isSuper() 判断当前登录用户名是否等于该配置。超级管理员会直接通过默认权限检查,不需要分配权限组。
注解规则
节点注解写在控制器类或公共方法的 PHPDoc 中。当前实现只识别以下形式:
| 注解 | 作用 |
|---|---|
@auth true | 该方法需要授权,进入权限树并由 system_auth_node 控制访问 |
@menu true | 该节点进入菜单表单的可选节点提示 |
@login true | 该方法仅要求后台登录,不要求权限组授权 |
注意:
- 只识别
true,不要写成@auth 1、@auth yes或只有@auth。 @auth true表示权限控制;未登录访问时也会因为无授权节点而跳转登录。@login true只做登录检查,不会进入权限树。@menu true只提供菜单节点候选,不会自动创建菜单。- 节点标题来自 PHPDoc 的第一行说明;如果第一行就是注解,则使用类名或方法名作为默认标题。
常见写法:
/**
* 系统用户管理
* @auth true
* @menu true
*/
public function index()
{
SystemUser::mQuery()->layTable();
}
/**
* 添加系统用户
* @auth true
*/
public function add()
{
SystemUser::mForm('form');
}
/**
* 修改当前用户资料
* @login true
*/
public function info()
{
// 仅要求后台登录
}权限检查流程
后台路由会注册 think\admin\support\middleware\RbacAccess。请求进入后台控制器时,大致流程如下:
请求进入路由中间件
|
|-- 当前应用在 rbac_ignore 中:直接放行
|
|-- AdminService::check() 通过:继续执行控制器
|
|-- 已登录但无权限:返回 code=0,提示“禁用访问!”
|
|-- 未登录或会话失效:返回登录地址AdminService::check() 的默认逻辑:
- 使用
NodeService::fullNode()将传入节点补全成完整节点。 - 读取
NodeService::getMethods()的节点扫描结果。 - 读取 Session 中的
user.nodes。 - 若存在
AdminService::registerCheckCallable()注册的回调,会依次执行所有回调;任一回调返回false即拒绝,全部不返回false时直接通过,不再进入后续默认检查。 - 若存在全局函数
admin_check_filter(),使用该函数结果作为检查结果,不再进入后续默认检查。 - 超级管理员直接通过。
- 目标节点未标注
@auth true时不做权限组校验;若标注了@login true,则要求已登录。 - 目标节点标注
@auth true时,必须存在于当前用户的user.nodes。
因此,auth() 判断只适合用于已扫描且已标注 @auth true 的节点。未知节点或未标注授权的节点不会被当作受控节点。
auth() 节点写法
全局函数 auth() 等同于:
\think\admin\service\AdminService::check($node);它支持相对节点补全:
| 写法 | 含义 |
|---|---|
auth(null) 或 auth('') | 检查当前请求节点 |
auth('remove') | 检查当前控制器的 remove 方法 |
auth('user/remove') | 检查当前应用下的 user/remove |
auth('admin/user/remove') | 检查完整节点 |
auth('shop.goods.cate/index') | 检查当前应用下多级控制器节点 |
模板中常用于控制按钮显示:
<!--{if auth("add")}-->
<button data-modal="{:url('add')}" class="layui-btn layui-btn-sm">添加</button>
<!--{/if}-->
<!--{if auth("remove")}-->
<button data-action="{:url('remove')}" data-value="id#{{d.id}}" class="layui-btn layui-btn-sm layui-btn-danger">删除</button>
<!--{/if}-->这类按钮级控制是模板显隐控制,真正的服务端访问仍由目标方法的 @auth true 和 RBAC 中间件拦截。
权限配置
配置应用名称
权限树展示应用名称时会读取 config/app.php 的 app_names,未配置时显示应用代码或插件名称。
return [
'app_names' => [
'admin' => '系统管理',
'plugin-wemall' => '微商城管理',
],
];忽略应用
rbac_ignore 中的应用不会被 RBAC 中间件检查,也不会参与节点扫描。
return [
'rbac_ignore' => ['index', 'api'],
];只应把确实不走后台会话权限的应用加入该配置。
登录地址
未登录或会话失效时,中间件会返回登录地址。默认使用 admin/login/index,也可以通过 rbac_login 调整:
return [
'rbac_login' => 'admin/login/index',
];若配置值以 /、http:// 或 https:// 开头,会按原始地址返回;否则会通过 sysuri() 生成系统地址。
后台操作步骤
- 在控制器方法中补充
@auth true。 - 打开“系统权限管理”,新增或编辑权限组。
- 在权限组中勾选需要授权的功能节点。
- 打开“系统用户管理”,给用户分配权限组。
- 用户重新登录,或重新调用
AdminService::apply(true)刷新 Session 授权。

菜单权限
菜单数据来自 system_menu,后台展示时由 MenuService::getTree() 过滤。
菜单过滤规则:
- 只读取
status = 1的菜单。 - 会先执行可选的全局
admin_menu_filter()。 - 有子菜单的节点会保留为分组,最终 URL 会置为
#。 - 叶子菜单的
url为空或#时会被移除。 - 如果菜单配置了
node,会先检查AdminService::check($menu['node'])。 - 内部 URL 会先用
/分割并取前三段作为节点再次检查,再通过admuri()生成后台 Hash URL。 - 外部 URL 不生成后台地址,只在带
params时拼接查询参数。
菜单表单中的“节点”候选来自 MenuService::getList(),只列出标注了 @menu true 的节点。
/**
* 系统菜单管理
* @auth true
* @menu true
*/
public function index()
{
SystemMenu::mQuery()->layTable();
}菜单层级最多选择到三级以内的父级,带实际 URL 的菜单不能再作为父菜单。

用户权限
后台创建或编辑用户时,除超级管理员账号外,系统要求给用户选择至少一个权限组。保存时权限组 ID 会写入 system_user.authorize。
登录流程会做以下处理:
- 校验账号、状态、密码和验证码。
- 将用户基础信息写入 Session 的
user。 - 更新登录次数、登录时间和登录 IP。
- 调用
AdminService::apply(true)刷新当前用户节点。
禁用的权限组不会生效。删除权限组时,对应的 system_auth_node 授权关系会同步删除。

缓存与刷新
控制器节点扫描结果会缓存在运行缓存中,缓存键为 SystemAuthNode,同一次请求中还会通过 sysvar('think.admin.methods') 复用。
可以使用以下方式刷新节点缓存:
\think\admin\service\AdminService::clear();在调试模式下,打开权限表单或菜单表单时,后台会自动清理节点缓存并重新扫描。
用户授权节点保存在 Session 中,修改用户权限后通常需要用户重新登录;也可以在当前会话中调用:
\think\admin\service\AdminService::apply(true);扩展点
默认权限检查可以通过两种方式扩展:
use think\admin\service\AdminService;
AdminService::registerCheckCallable(function (string $current, array $methods, array $userNodes) {
// 返回 false 表示拒绝;不返回 false 时会短路通过,不再执行默认检查
});或定义全局函数:
function admin_check_filter(string $current, array $methods, array $userNodes): bool
{
return in_array($current, $userNodes, true);
}菜单数据也可以通过全局函数处理:
function admin_menu_filter(array $menus): array
{
return $menus;
}节点扫描结果可以通过全局函数处理:
function admin_node_filter(array $nodes): array
{
return $nodes;
}常见问题
节点没有出现在权限配置中
优先检查以下几点:
- 方法必须是控制器中的
public方法。 - 方法注释必须写
@auth true。 - 当前应用不能在
rbac_ignore中。 - 控制器类可以被正常自动加载。
- 清理节点缓存后重新打开权限配置。
菜单节点没有出现在候选列表中
菜单候选列表只读取 @menu true 节点。若只写了 @auth true,节点可以进入权限树,但不会出现在菜单候选列表。
已经分配权限但仍然无权访问
可能原因:
- 用户未重新登录,Session 中的
user.nodes仍是旧数据。 - 权限组状态被禁用。
- 控制器节点缓存未刷新,节点名与实际访问节点不一致。
- 菜单 URL 指向的前三段节点没有授权。
- 目标方法不是实际被访问的方法,例如前端请求到了其他 API 节点。
auth('remove') 为什么没有拦住按钮
auth('remove') 会补全为当前控制器的 remove 节点。它依赖目标方法被扫描,并且目标方法标注 @auth true。如果节点不存在或未标注 @auth true,默认检查不会把它当作受控权限。
@auth true 和 @login true 如何选择
后台业务操作通常使用 @auth true。只要登录即可访问、无需分配权限组的接口才使用 @login true,例如后台首页个人资料、已登录接口等。
