权限码必须落到具体接口动作,不能只靠路由节点;需定义语义化权限码(如article:delete:draft)并存入auth_rule.rule_code,校验时显式传入且大小写、符号须完全一致;api场景禁用session,改用jwt或redis缓存;字段级脱敏在数据组装阶段干预,数据范围过滤须显式调用scope或封装joinauth方法;权限变更后必须强清理user_permissions_{$uid}和auth_rules_all缓存。

权限码必须落到具体接口动作,不能只靠路由节点
ThinkPHP 的 AuthRule::check() 默认只比对当前控制器/方法名(如 ArticleController/delete),但真实业务中,“删除”这个动作可能分场景:普通编辑员只能删草稿,审核员能删已发布文章,HR 只能删自己部门员工记录。光靠路由节点无法区分。
正确做法是定义带语义的权限码,例如:article:delete:draft、article:delete:published、user:delete:own_dept,并存入 auth_rule.rule_code 字段(加唯一索引)。路由层不自动映射,需在控制器方法开头显式传入:
if (!AuthRule::check('article:delete:draft', $this->userId)) {
throw new HttpException(403);
}
- 别依赖
$this->request->action()拼接权限码——它返回的是delete,不是完整上下文 - 权限码大小写、冒号、下划线必须和数据库里
rule_code完全一致,多一个空格就失效 - API 场景禁用 Session,改从 JWT payload 读
permissions数组,或查库 + Redis 缓存(key 为user_permissions_{$uid})
字段级脱敏必须在数据组装阶段干预,不能靠中间件拦截
按钮可见、接口可调用 ≠ 数据可看全。比如财务系统中,“实发工资”字段只对 HRBP 可见,其他人应看到 **** 或 null。这类控制无法用 AuthRule::check() 解决,因为校验发生在请求入口,而字段过滤发生在数据输出前。
推荐在模型的 toArray() 或资源类(Resource)的 toArray() 中做判断:
public function toArray($request)
{
$data = parent::toArray($request);
if (!AuthRule::check('salary:show:real', $this->user_id)) {
$data['real_salary'] = '****';
}
return $data;
}
- 不要在控制器里手动 unset 字段——容易漏、难复用、违反单一职责
- 避免在数据库查询时就用
SELECT CASE WHEN ...过滤,会导致 SQL 复杂、缓存失效、ORM 聚合失效 - 若用 JSON 返回,字段脱敏后仍要确保序列化前已处理,否则
json_encode()会暴露原始值
数据范围过滤必须用 scope + 显式调用,且 JOIN 时不自动下推
ThinkPHP 的 scope 不是“全局生效”,而是“按需启用”的查询预设。很多人写了 protected $scope = ['auth' => function ($query, $uid) { ... }],却在代码里直接写 UserModel::where(...)->select(),结果权限完全没起作用。
必须显式调用:
UserModel::scope('auth', $this->userId)->where('status', 1)->select();
- 闭包 scope 才能传参;静态方法
scopeAuth()拿不到登录用户,硬编码会越权或拦截失效 -
withScope()是替换而非叠加,withScope(['auth'])会清掉默认 scope,慎用 - JOIN 查询中,scope 只作用于主表,子表(如
order、product)需手动补条件:->join('order o', 'u.id = o.user_id AND o.status IN (0,1)') - 更安全的做法是封装
joinAuth()方法,在里面统一处理各表的可见性逻辑
权限变更后缓存必须强清理,尤其注意 Swoole 场景
用户刚被分配新权限,前端按钮仍灰、接口仍 403,90% 是缓存没清干净。Redis 缓存 key 要覆盖两个维度:user_permissions_{$uid} 和 auth_rules_all(用于权限码全量映射)。
所有后台权限变更操作(增删角色权限、用户换角色)都必须同步执行:
cache('user_permissions_' . $userId, null);
cache('auth_rules_all', null);
- 别用
static $cache = []做本地内存缓存——Swoole/Workerman 下进程不共享,部分请求永远读到旧权限 - TP6.0+ 的
think-auth扩展默认不监听权限变更事件,得自己在管理后台的权限保存逻辑里加清理代码 - 调试时可在
checkPermission()开头加日志,输出实际查的缓存 key 和返回值,比猜快得多
细粒度控制最易被忽略的点是:权限码定义、数据过滤时机、缓存生命周期这三者必须严格对齐。差一环,要么放行越权,要么误拦合法请求。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











