数据脱敏必须在模型层get*Attr访问器中实现,才能覆盖toArray、toJson、Excel导出、日志打印等所有输出场景;手机号与邮箱需分别做清洗替换和分段处理,并支持按角色动态控制脱敏粒度。

ThinkPHP 8.1 中做数据脱敏,核心原则是:脱敏必须发生在数据“输出前”的最后一环,而模型层的 get*Attr 访问器是唯一能稳定覆盖所有场景的机制。
模型访问器才是脱敏主战场
中间件、控制器或全局响应拦截,都无法真正拦住以下常见输出路径:
-
$user->toArray()、$user->toJson()、json($user)—— 这些直接调用模型序列化,绕过中间件 - Excel 导出时调用
$list->each(...)或$list->column('mobile') - 日志中写
Log::info('user info', $user->toArray()),字段仍是明文 - 关联查询如
User::with('profile')->find(1),子模型字段不会被主模型中间件影响
只有在模型里定义 getMobileAttr、getEmailAttr 等方法,才能确保每次读取该字段(无论是模板渲染、API返回、导出还是日志打印)都自动走脱敏逻辑。
手机号与邮箱脱敏要兼顾健壮性
字段名决定访问器命名,不能错:
修复 Core bailout 状态损坏安全问题,以及 BZ2、DOM、PDO、Opcache/JIT 内存与执行错误,改进 Unicode 字素处理、URL 解析、TLS 流状态、信号分发及会话错误处理。
立即学习“PHP免费学习笔记(深入)”;
- 字段叫
mobile→ 必须写getMobileAttr($value) - 字段叫
email→ 必须写getEmailAttr($value)
脱敏代码需防崩、防误判:
- 开头加空值判断:
if (empty($value) || !is_string($value)) { return $value; } - 手机号推荐清洗后替换:
$clean = preg_replace('/[^\d+]/', '', $value); return substr_replace($clean, '****', 3, 4);(兼容 +86、空格等) - 邮箱必须拆分处理:
[$local, $domain] = explode('@', $value, 2) ?: [$value, '']; return $local ? substr($local, 0, 1) . str_repeat('*', strlen($local) - 2) . substr($local, -1) : $value . '@' . $domain;
按角色动态控制脱敏粒度
静态隐藏($hidden)或统一脱敏都不够灵活。真要实现「管理员看全量、普通用户只看脱敏」,得在查询阶段注入差异逻辑:
- 用
withAttr()配合白名单:User::withAttr('id_card', fn($v) => auth()->user()->can('view_id_card') ? $v : '****').find(1) - 或在模型的
get*Attr中主动判断:if (auth()->check() && auth()->user()->can('view_id_card')) { return $value; } - 避免在模型里写
protected $hidden = ['id_card']—— 这等于暴露字段存在,且无法做权限区分
日志与导出必须单独兜底
模型访问器虽强,但日志和导出常走原生数组,容易漏掉:
- 订单/用户相关日志,必须封装统一函数如
write_user_log($data),内部调用User::make($data)->visible(['name', 'mobile'])->toArray()或手动脱敏 - Excel 导出不要直接传
$users->toArray(),改用$users->map(fn($u) => ['name' => $u->name, 'mobile' => $u->mobile]),让访问器生效 - 禁止在控制器里写
Log::info('login', $request->param())—— 参数可能含密码,应先过滤再记录


















