thinkphp日志脱敏必须在写入前对$data结构化过滤,因log::write()不识别$hidden规则、不调用访问器、不自动清洗;直接记录$request->param()会导致密码等明文泄露。

ThinkPHP 日志脱敏不能靠配置文件开关,必须在写入前对 $data 做结构化过滤——Log::write() 不识别敏感字段,也不会自动调用模型访问器或 $hidden 规则。
为什么 log::write($request->param()) 会泄露密码
因为 Log::write() 接收什么就记什么,它不关心你模型里有没有 $hidden = ['password'],也不管请求参数里是不是含 access_token。常见错误是直接把 $request->param() 或 $request->post() 塞进去,结果日志里明文出现 "password": "123456"。
-
$request->param()返回的是原始数组,和input()行为一致,不做任何清洗 - 日志处理器(如
think\log\driver\File)默认不启用递归遍历,嵌套结构如['user' => ['password' => 'xxx']]完全不会被处理 - 别指望中间件统一拦截——日志可能来自命令行、定时任务或异常捕获,这些场景中间件不生效
用自定义日志处理器做全局脱敏
这是最可靠的方式:继承 think\log\driver\File,重写 save() 方法,在写入磁盘前对 $data 执行脱敏。不是所有地方都适合改全局处理器,但登录、支付、用户资料类接口强烈建议这么做。
- 在
app/common.php或独立日志配置中注册:Log::setHandler(new \app\common\log\SecureFile()); - 脱敏函数必须递归处理:
password、pwd、token、id_card、bank_card、mobile、phone等 key 名需大小写不敏感匹配 - 避免手写深度递归,用
array_walk_recursive()更安全,防止栈溢出;若需保留键路径(如区分user.phone和order.phone),才用自定义递归 - 脱敏值统一设为
'***'或'[REDACTED]',别用空字符串——后者可能被误认为字段缺失
临时脱敏:Log::record() + 预处理更轻量
如果你只对某几个接口做脱敏,或者想控制脱敏粒度(比如只过滤 password,但保留 user_id),Log::record() 是更灵活的选择。它不走默认日志通道,也就绕过了未改造的处理器逻辑。
- 先调用脱敏函数:
$params = maskSensitiveData($request->param()); - 再记录:
Log::record('登录请求参数: ' . json_encode($params, JSON_UNESCAPED_UNICODE), 'info'); - 注意:
Log::record()第二个参数是日志级别,第三个参数可传上下文,但不会触发 formatter,所以 JSON 编码必须自己做,且要加JSON_UNESCAPED_UNICODE防中文乱码 - 别在控制器里写
unset($params['password'])——容易漏、难覆盖、测试难,且嵌套结构照样失效
脱敏后还要防 JSON 输出反向暴露
很多人做完数组脱敏,最后用 json_encode() 一输出,结果敏感字段又回来了。这是因为脱敏只作用于你传进去的那个数组副本,而 json_encode() 对象时会重新读取原始属性。
- 如果日志内容来自模型对象(如
$user->toArray()),必须确保脱敏发生在toArray()之后、json_encode()之前 - 别在模型里用
getPasswordAttr()期望它影响日志——访问器只对模型属性读取生效,Log::write($user)是直接序列化对象,不触发访问器 - 已写入的日志文件无法“事后脱敏”——
sed或 PHP 脚本批量替换只能应急,且无法修复已被轮转、压缩或上传到 ELK 的数据
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











