ThinkPHP无开箱即用数据权限,须在查询时显式注入条件;AuthRule::check()仅校验路由权限,不控制数据行级访问;需为不同角色、关联模型分别配置过滤规则,并分离数据权限与字段脱敏。

ThinkPHP 本身不提供开箱即用的「数据权限分级」能力,所谓“按部门查自己数据”“仅看自己创建的记录”这类控制,必须在查询构建阶段手动注入条件,而不是靠模型 hidden 或 AuthRule::check() 解决。
数据权限不能靠 AuthRule::check() 实现
AuthRule::check() 只能判断「用户有没有访问 /admin/user/edit 这个路由的权限」,它不关心你查的是谁的数据。比如一个普通编辑登录后调用 UserModel::find(100),即使他没权限看 ID=100 的用户,AuthRule::check() 也完全不会拦截——因为它只校验路由/方法名,不校验数据上下文。
常见错误是把数据过滤逻辑塞进中间件或基类控制器的 initialize() 里,结果所有查询都被统一加了 where('user_id', $uid),导致管理员也查不到别人数据。
- 真正可行的做法:在每个需要数据权限控制的查询处,显式调用权限服务获取过滤条件
- 例如封装一个
DataScope::for('user'),返回['user_id' => $uid]或['dept_id' => ['in', [1,2,5]]] - 然后手动拼进查询:
UserModel::where(DataScope::for('user'))->select() - 别依赖全局 scope —— 管理员、审计员等角色需要绕过时没法 disable
用 with(['relation']) 时数据权限容易失效
关联查询(如 UserModel::with('profile')->find(1))中,主模型和关联模型的权限是独立的。你给 User 加了 where('user_id', $uid),但 Profile 表根本没加任何限制,照样能查出全部 profile 记录。
立即学习“PHP免费学习笔记(深入)”;
这是因为 with() 默认走独立查询,不会继承主模型的 where 条件。
- 必须显式控制关联模型的查询:用闭包方式重写关联定义,例如
->with(['profile' => function ($q) { $q->where(DataScope::for('profile')); }]) - 或者改用 join 查询,把权限条件统一写在 on 或 where 里
- 注意:
belongsTo关联的外键字段(如profile.user_id)可能被模型自动处理,但权限字段(如profile.dept_id)不会自动对齐,得自己补
toArray() 和 JSON 输出时字段脱敏与数据权限是两回事
很多人混淆「不让前端看到 phone 字段」和「不让前端查到别人的 phone 数据」。前者是字段级脱敏,后者是数据行级隔离。两者必须分开处理,且顺序不能错:先做数据权限过滤(查出合法数据),再做字段脱敏(隐藏敏感字段)。
典型翻车场景:在模型里用 withAttr('phone', fn() => '***') 做脱敏,但没做数据权限控制,结果前端虽然看不到明文 phone,却能通过 ID 列表批量猜出所有人手机号对应关系。
-
toArray()触发hidden和withAttr,但不触发数据权限逻辑 -
json()方法默认跳过模型转换,直接序列化原始属性,hidden和withAttr全部失效 - 如果用了
collection->all()或强制类型转换(array) $collection,连hidden都不走 - 正确姿势:查完数据 → 用
UserDataMasker::mask($data, $permissions)统一脱敏 → 再json()输出
数据权限的核心难点不在怎么写 SQL,而在于「权限策略如何随业务变化快速调整」——比如从「按创建人隔离」升级为「按部门+岗位双维度可见」,硬编码的 where 条件会迅速失控。建议把数据权限规则收口到独立服务,用配置驱动(如 YAML 描述规则),而非散落在几十个控制器里手写 where。



















