真正能落地的简单方案是收口到中间件+独立日志渠道+结构化字段;ThinkPHP 6 应用 AdminLogMiddleware 统一拦截后台请求,记录 user_id、action、input、status、session_id 五字段,日志写入数据库并脱敏处理。

直接用 file_put_contents 记后台操作日志,90% 的项目会在上线后两周内出现日志丢失、格式错乱或权限泄露问题。真正能落地的简单方案,是收口到中间件 + 独立日志渠道 + 结构化字段,不依赖框架 AOP 或复杂代理。
ThinkPHP 6 用中间件统一拦截所有后台请求
别在每个控制器里手动写 Log::write(),也别折腾注解式 AOP——TP6 原生不支持,硬套只会让逻辑散落在各处且漏掉异常分支。
- 新建
app/middleware/AdminLogMiddleware.php,在handle()中调用$next($request)前记录请求入口,在after()中捕获结果 - 必须显式使用
Log::channel('admin'),否则日志全进默认single渠道,查不到也归不了档 - 敏感操作(如删除、状态变更)要结合
$request->isDelete()或$request->isPost()判断,避免 GET 请求带参数触发误记 - 用户 ID 必须通过
Auth::id()或$request->attr('user')['id']获取,不能读session('user_id')—— CLI 或 API 场景会崩
日志内容必须含这 5 个字段,缺一不可
只记 URL 和 IP 是审计黑洞。回溯时你根本不知道谁删了哪条数据,更没法关联行为链。
-
user_id:登录态用户 ID;未登录则 fallback 到$request->ip() -
action:固定为$request->controller() . '/' . $request->action(),比解析路由稳定 -
input:用array_intersect_key($request->param(), array_flip(['id', 'status', 'role_id']))过滤,剔除password、token等敏感键 -
status:不能依赖$response->getCode()—— 异常未捕获时它永远是 200;正确做法是 catchValidateException/DbException,或检查响应体是否含'code' => 0 -
session_id:比 IP 更可靠,同一出口下多个账号共用时能精准区分人
日志写到数据库,别写文件
文件日志查起来慢、没索引、权限难控、无法关联查询。审计日志不是调试日志,它得能被快速检索和导出。
立即学习“PHP免费学习笔记(深入)”;
- 配置
config/log.php中的admin渠道:'type' => 'database',指向专用日志表 - 日志表字段至少含:
user_id、ip、method、url、input(JSON 格式)、result_code、created_at -
input字段必须用json_encode($params, JSON_UNESCAPED_UNICODE),但前提是已过滤敏感键,否则密码明文入库 - 千万别在中间件里直接执行
Db::insert()—— 落库失败会拖垮整个响应;应交由 Log 驱动异步处理
Monolog 不是开箱即用,得禁用默认格式器
Monolog 默认把整个 $context 原样输出,如果业务代码不小心把用户密码塞进 context,日志文件就等于公开了凭证。
- 初始化 handler 时,必须调用
$handler->setFormatter(new LineFormatter(...)),并手动控制输出字段 - 推荐格式:
"%datetime% | %channel% | %level_name% | %message% | user=%context[user_id] action=%context[action] status=%context[status]\n" - 确保
Logger实例用独立 channel:$auditLogger = new Logger('audit'),避免和 error/debug 日志混在一起 - 日志路径必须放在 Web 根目录外,比如
/var/log/myapp/audit.log;否则攻击者可通过../audit.log直接下载
最易被忽略的一点:所有日志字段都得经脱敏处理,哪怕只是记录用户名,也要确认它没被前端传入恶意 HTML 或 SQL 片段;htmlspecialchars() 或 filter_var($str, FILTER_SANITIZE_STRING) 得在写入前跑一遍,否则日志系统本身可能成为 XSS 或注入入口。



















