ThinkPHP Auth类不适用于模块化场景,因其仅支持简单规则字符串匹配,无法识别模块/控制器/操作三层结构、请求方法及动态权限节点,导致多模块下规则冗余、校验失效;需自建基于Route与四元组规则表的中间件校验逻辑。

ThinkPHP 的 Auth 类为什么不能直接用在模块化场景中
因为 Auth 类默认只支持基于规则字符串(如 admin/article/edit)的简单匹配,不感知模块、控制器、操作三层结构,也不区分请求方法(GET/POST),更不支持运行时动态加载权限节点。一旦项目拆分成多模块(admin、api、wechat),硬套 Auth 会导致规则冗余、校验失效或误放行。
实际踩坑现象包括:Auth::check() 总返回 false(规则没注册)、切换模块后权限丢失(规则未带模块前缀)、saveRule 报错 Undefined index: name(规则数据格式不对)。
真正可行的路径是绕过 Auth,自己构建基于 think\Route 和数据库规则表的中间件校验逻辑。
如何定义模块化权限规则表结构
必须把「模块-控制器-操作-请求方法」四元组作为最小权限单元,否则无法支撑细粒度控制。推荐表结构如下:
立即学习“PHP免费学习笔记(深入)”;
CREATE TABLE `auth_rule` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `module` varchar(32) DEFAULT 'index', -- 模块名,不能为空 `controller` varchar(64) NOT NULL, -- 控制器名(不含后缀) `action` varchar(64) NOT NULL, -- 方法名 `method` varchar(10) DEFAULT 'ANY', -- GET/POST/ANY `title` varchar(100) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_module_ctrl_act_method` (`module`,`controller`,`action`,`method`) );
关键点:
-
module字段不可为空,默认值设为index,避免漏填导致全站失效 -
method支持ANY表示不限请求方式,但生产环境建议显式声明GET或POST - 不要加
status字段做软删除——权限失效应通过移除角色绑定实现,而非隐藏规则 - 查询时务必用联合唯一索引
uk_module_ctrl_act_method,否则并发校验会慢
权限中间件怎么从路由信息提取模块/控制器/操作
ThinkPHP 6+ 的中间件里不能直接依赖 $request->action(),因为 URL 可能被路由规则重写(比如 /article/1 → index/article/read)。必须用 think\Route 解析出真实调度目标:
use think\facade\Route;
<p>$dispatch = app('http')->getDispatch();
if ($dispatch instanceof \think\route\Dispatch && $dispatch->type() === 'module') {
$module = $dispatch->module();
$controller = $dispatch->controller();
$action = $dispatch->action();
$method = $request->method();
} else {
// 非模块路由(如闭包路由、资源路由)走白名单或拒绝
return $next($request);
}</p>注意陷阱:
-
$dispatch->controller()返回的是类名(如ArticleController),需用Str::snake()转成小写蛇形(article)再入库和比对 - 如果用了多级命名空间(如
app\admin\controller\User),$dispatch->controller()仍只返回User,模块名已由$dispatch->module()单独提供,无需拼接 - RESTful 资源路由(
Route::resource('user', 'User'))会生成多个动作(index/create/save),需在迁移脚本里批量插入对应规则
角色-规则关联查询为什么不能用 with 预载,而要用子查询
权限校验是高频操作,每请求一次都要查「当前用户所有角色拥有的全部规则」。如果用 User::with('roles.rules'),会触发 N+1 查询(先查用户,再查角色,再为每个角色查规则),且结果集无法去重(同一规则可能被多个角色重复赋予)。
正确做法是单条 SQL 子查询:
SELECT COUNT(*) > 0 AS has_access
FROM auth_rule r
WHERE r.module = ?
AND r.controller = ?
AND r.action = ?
AND (r.method = ? OR r.method = 'ANY')
AND r.id IN (
SELECT rule_id FROM auth_role_rule
WHERE role_id IN (
SELECT role_id FROM auth_user_role
WHERE user_id = ?
)
)
这样一条语句完成全部判断,MySQL 5.7+ 下执行时间稳定在 1–3ms。别忘了给 auth_role_rule.rule_id 和 auth_user_role.role_id 加索引,否则子查询会变全表扫。
这个查询逻辑要封装进一个独立服务类(如 PermissionService),中间件里只调 PermissionService::allow($module, $controller, $action, $method),避免 SQL 散落在各处。
最常被忽略的一点:模块名大小写必须统一。开发时习惯用 Admin,但数据库存的是小写 admin,中间件里没做 strtolower() 就会导致永远无权限。



















