审计日志必须在路由解析后、控制器执行前的中间件中统一记录,确保获取用户id、动作标识等完整上下文,字段需包含user_id、action、method、ip、status、params(白名单脱敏)、before_data(高危操作快照)及session_id,写入须用独立db连接与联合索引,并辅以行为钩子处理登录登出等非标准路径事件。

直接用 Log::write() 记用户操作日志,等于没做审计——查不到谁干的、看不出参数意图、分不清成功失败,还会和错误日志混在一起,后期根本没法回溯。
中间件必须在路由解析后、控制器执行前拦截
这是唯一能稳定拿到 $request->action()、$request->controller() 和认证用户 ID 的位置。控制器里写、基类 initialize() 里写、甚至 AOP 式切面,都会漏掉异常分支或拿不到完整上下文。
- 注册点必须是
app/middleware.php全局生效,别塞进某个路由分组里 - 获取用户 ID 要分场景:
Auth::id()(RBAC 场景)、$request->attr('user')['id'](JWT 中间件已注入)、兜底读X-User-IDHeader 并校验签名,不能直接用session('user_id') - 敏感接口如登录、登出、验证码,必须用
in_array($request->action(), $ignoreList)过滤,靠 URL 匹配容易漏判 - 记录动作标识推荐用
$request->rule()->getRuleName(),比解析 URL 稳定,尤其在带参数的路由下
日志字段不能只记 URL 和 IP
只存 $request->url() 和 $request->ip() 在真实审计中基本无效。攻击者改个 ID 参数就能绕过,你连他删的是张三还是李四都确认不了。
- 必填字段:
user_id(整型,NULL 允许匿名)、action(如admin/user/delete)、method(POST/PUT)、ip(VARCHAR(45),兼容 IPv6)、status("success"或"failed")、created_at -
params字段必须脱敏:用array_intersect_key($params, array_flip(['id', 'status', 'role_id']))白名单提取,别用array_filter()黑名单剔除——漏一个就全暴露 - 高危操作(如删除、权限变更)要额外记录快照:
before_data存 JSON,比如UserModel::find($id)返回的用户名、邮箱、状态 - 别忽略
session_id():同一 IP 多账号共用时,它是区分操作人的关键
数据库写入必须脱离主事务且建对索引
把日志插入和业务逻辑塞进同一个事务,等于给主流程埋雷——日志表写挂了,订单也提交不了;索引建错,查“张三昨天改过哪些配置”就得等 3 秒以上。
- 写入必须用独立 DB 连接:
Db::connect('log_db')->name('audit_log')->insert($data),不能复用主库连接 -
created_at单独建索引不够,高频查询是WHERE user_id = ? AND created_at > ?,必须建联合索引:ALTER TABLE audit_log ADD INDEX idx_user_time (user_id, created_at); - 表引擎必须是
InnoDB,MyISAM的COUNT(*)是全表扫,分页直接卡死 - 别信“异步队列能解决性能问题”——
think-queue延迟入库会导致“操作成功但日志没生成”,审计链路断档;真要异步,用file_put_contents(..., FILE_APPEND | LOCK_EX)或 Redis List + 独立消费者进程
行为钩子只适合离散关键事件
登录成功、密码修改、登出这类事件,不适合塞进中间件统一处理,因为它们不走标准 HTTP 动作路径,时机也不固定。行为钩子是解耦的最佳选择,但极易配置失效。
- 触发点必须在 Session 写入之后,例如登录成功分支末尾加:
Hook::listen('login_success', ['uid' => $user['id'], 'username' => $user['username'], 'ip' => request()->ip()]) - 行为类路径必须是
app\common\behavior\LoginLogBehavior,且app\provider.php中显式返回该类路径,ThinkPHP 不会自动扫描 - 钩子名大小写敏感,
Hook::listen('Login_Success')和login_success是两个钩子 - 行为类
run()方法里禁止任何耗时操作(发 HTTP、写文件),否则拖慢登录响应;入库建议走独立连接或缓存暂存
真正落地的审计不是字段堆得多,而是每个字段都有明确用途、每条日志都能闭环验证——比如“失败日志”必须由全局异常处理器补写,而不是依赖中间件 after();比如“快照”只在规则匹配时触发,避免全量查询拖垮性能。这些细节不写进代码,上线后第一起安全事件就会暴露出来。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











