应用层动态脱敏必须在模型访问器、查询构造器或服务层实现,不可依赖数据库配置或sql层,因其会破坏权限逻辑可维护性、导致原生查询失效、字段名差异引发匹配失败,且下游导出日志等场景无法继承脱敏结果。

应用层动态脱敏在 ThinkPHP 中不能靠数据库配置实现,必须在模型访问器(get*Attr)、查询构造器(field裁剪)或服务层(如 UserDataMasker)中控制——数据库层做脱敏会破坏权限逻辑的可维护性与调试能力。
为什么不能在数据库配置或 SQL 层做脱敏
MySQL 视图、CASE WHEN 或原生 SELECT 中用 CONCAT 掩码,看似省事,实则把权限判断下推到 DB 层。一旦角色规则变更(比如「客服可看手机号前 4 位」),就得改 SQL、重写视图、同步所有关联查询,且无法复用 PHP 的 RBAC 权限码(如 user:read:phone)。更严重的是,Db::table()->select() 这类原生查询完全绕过模型逻辑,脱敏直接失效。
- 数据库字段名大小写、前缀、严格模式差异会导致策略匹配失败(如本地
user_namevs 生产UserName) -
withAttr()和访问器只对 ORM 查询生效,原生查询不触发 - 导出 Excel、日志记录、API 响应等下游环节无法继承 SQL 层脱敏结果
用 get*Attr 实现全局读取脱敏
这是最稳定、覆盖最全的方式:只要字段被读取($user->phone、$user->toArray()、$user->toJson()),就自动走脱敏逻辑。
- 函数名必须严格为
getPhoneAttr(字段名首字母大写 +Attr),否则不触发 - 必须判空:
if (empty($value) || !is_string($value)) { return $value; },避免null或数字类型报错 - 推荐用
substr_replace($value, '****', 3, 4)替代正则,性能高、无 PCRE 依赖、适配 11 位手机号 - 国际号需先清洗:
$clean = preg_replace('/[^0-9]/', '', $value); return substr_replace($clean, '****', 3, 4); - 邮箱脱敏要拆分
@前后,仅掩码本地部分:$local[0] . str_repeat('*', max(0, strlen($local)-2)) . $local[-1]
用 field 动态裁剪实现权限级字段过滤
真正从源头控制权限——不是“显示时藏起来”,而是“查的时候就不取”。适合后台导出、高敏感接口等场景。
- 根据当前用户权限码决定 SELECT 字段:
User::field(['id', 'name', 'email'])->where(...)->select() - 普通用户:
field(['id', 'name']);管理员:field('*')或显式列出全部 - 关联查询需分别处理:
User::with(['profile' => function ($q) { $q->field(['user_id', 'realname']); }]) - 注意:
field()不影响模型属性定义,$user->phone仍可读(但值为null),避免后续误用
导出和日志场景最容易漏脱敏
getData()、toArray(true)、collection->all()、json_encode($model) 全部绕过 get*Attr 和 $hidden,必须手动补救。
- 导出前统一处理:
$list->each(fn($u) => $u->mobile = hide_mobile($u->mobile)); - 日志记录禁用
$order->toArray(),改用白名单:array_intersect_key($order->toArray(), array_flip(['id', 'name', 'email'])) - 若用
withOutput()全局 JSON 脱敏,Desensitize::handle()必须支持嵌套键(如user.profile.phone),不能只匹配一级phone - 测试时务必验证导出文件内容,别只信 API 返回——这是最常翻车的地方
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











