thinkphp手机号邮箱脱敏必须在模型层用访问器实现:手机号用getmobileattr配合substr_replace并清洗国际号,邮箱拆分后仅掩码本地部分首尾,关联模型需单独定义,导出和日志须手动处理,不可依赖配置或withattr。

ThinkPHP 里手机号、邮箱脱敏不能靠配置开关,得在模型层用访问器(Accessor)写死逻辑,否则 toArray()、导出、API 返回全会漏掉。
手机号脱敏必须用 getMobileAttr + substr_replace
直接写 substr($mobile, 0, 3) . '****' . substr($mobile, -4) 看似简单,但空值、非字符串、国际号(+86138…)都会崩。正确姿势是:
- 函数名严格为
getMobileAttr(字段名首字母大写 + Attr),否则不触发 - 必须判空:
if (empty($value) || !is_string($value)) { return $value; } - 用
substr_replace($value, '****', 3, 4)替换第4–7位,比正则快且无 PCRE 依赖 - 若存国际格式,先清洗数字:
$clean = preg_replace('/[^0-9]/', '', $value); return substr_replace($clean, '****', 3, 4);
邮箱脱敏不能只截用户名,得保留域名结构
邮箱掩码不是简单砍中间,zhang***@example.com 和 z***@e***.com 都错——前者易被猜中,后者破坏可读性。正确逻辑是:
- 拆分
@前后:[$local, $domain] = explode('@', $value, 2); - 只掩码本地部分中间字符:
strlen($local) > 2 ? $local[0] . str_repeat('*', strlen($local) - 2) . $local[-1] : $local - 域名保持原样,如
example.com不动,避免影响识别和调试 - 同样加空值判断,
!isset($local) || !$domain直接返回原始值
关联模型脱敏不会自动继承,每个子模型都要单独定义
比如 User 模型有 getMobileAttr,但 with('profile') 查出的 Profile 模型里的 phone 字段照样明文——ThinkPHP 不递归应用父模型的访问器。
- 必须在
Profile模型里也写一遍getPhoneAttr - 如果多个模型共用同一套脱敏规则,抽成 Trait 更稳妥:
use AnonymizeFields; -
withAttr只适用于单次查询临时覆盖,无法替代模型层访问器的全局生效能力 - 原生查询
Db::table('user')->select()完全绕过所有访问器,脱敏彻底失效
导出 Excel 或日志记录时,脱敏逻辑最容易被绕过
导出常用 $list->each() 或 getData(),这两个操作默认跳过访问器——getData('mobile') 返回明文,toArray(true) 也不触发 getMobileAttr。
- 导出前统一走 collection 处理:
$list->each(fn($u) => $u->mobile = hide_mobile($u->mobile)); - 日志记录绝不能传
$order->toArray(),必须先过滤白名单字段:['mobile', 'email', 'id_card'] - TP6.1+ 可用
$user->getAttr('mobile', null, false)显式禁用访问器,但仅限需要明文的极少数场景 - 测试时务必用真实数据跑一遍导出文件,确认 Excel 单元格里显示的是
138****5678而非原始号码
真正难的不是写几个星号替换,而是厘清哪些字段在哪些路径下必须脱敏、哪些接口允许明文、规则是否随角色变化——这些一旦散落在控制器、导出方法、日志封装里,下次改掩码长度就等于全线重测。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











