应通过中间件、行为钩子、数据库事件监听三类机制实现全链路操作审计:中间件统一记录HTTP请求,行为钩子捕获登录登出等关键业务事件,数据库监听器追踪SQL执行细节,三者协同覆盖用户行为全场景。

如果您在ThinkPHP应用中需留存可审计的用户操作痕迹,但当前系统未自动捕获“谁在何时执行了何种操作”,则说明日志逻辑尚未接入请求生命周期关键节点。以下是解决此问题的步骤:
一、通过中间件统一拦截HTTP请求
该方法在路由调度完成、控制器执行前捕获完整上下文,确保所有受控接口(除明确排除项外)均被覆盖,且能安全获取认证用户ID、请求参数、IP及动作标识。中间件位置稳定、逻辑集中,避免控制器内零散写入导致遗漏或格式不一。
1、在app/middleware.php中注册自定义中间件,例如AppMiddlewareOperationLogMiddleware::class。
2、在OperationLogMiddleware的handle()方法开头,调用$auth = app('thinkAuth');并执行$adminId = $auth->getUserID();获取经RBAC校验的真实管理员ID。
立即学习“PHP免费学习笔记(深入)”;
3、判断$adminId是否为有效数值,若为null或0,则跳过日志记录,不写入空用户或伪造ID。
4、使用$request->rule()->getModule()、$request->rule()->getController()和$request->action()提取模块、控制器与动作名,拼接为module/controller/action格式作为action字段值。
5、调用$request->param()获取全部输入参数,再通过array_filter()显式剔除['token', 'password', 'old_password', 'api_key']等敏感键名,保留脱敏后参数数组。
6、构造日志数据数组,包含admin_id、action、method($request->method())、ip(经多层校验后的真实客户端IPv4/IPv6地址)、params(JSON编码)、created_at(date('Y-m-d H:i:s')),并使用json_encode($logData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)序列化。
7、执行数据库插入,目标表须含admin_id(INT或NULL)、action(VARCHAR(100))、method(CHAR(10))、ip(VARCHAR(45))、params(JSON或TEXT)、created_at(DATETIME)字段,并为created_at和admin_id建立联合索引。
二、利用行为钩子记录登录/登出等关键节点
该方法适用于登录成功、登出、密码修改等由特定业务逻辑触发的离散事件,通过自定义钩子解耦日志代码与主流程,避免污染认证核心逻辑,同时保证事件发生时机精准可控。
1、在登录控制器(如app/controller/Login.php)的认证成功分支末尾,添加Hook::listen('login_success', ['uid' => $user['id'], 'username' => $user['username'], 'ip' => $realIp]),其中$realIp为经X-Forwarded-For等头信息校验后的结果。
2、创建行为类app/common/behavior/LoginLogBehavior.php,实现run方法,在内部仅处理传入参数,不执行耗时操作(如远程调用、文件写入)。
3、在run方法中,对传入参数进行基础校验,确认uid为整型、username为非空字符串、ip匹配IPv4/IPv6正则模式,任一不满足则直接返回,不写日志。
4、将校验通过的数据组装为最小审计字段集:包括uid、username、ip、login_time(date('Y-m-d H:i:s'))、status(固定为1)、user_agent($request->header('user-agent')),并过滤掉$_SERVER中可能存在的敏感头信息(如Authorization)。
5、调用Db::name('admin_login_log')->insert()同步写入数据库,目标表结构中ip字段必须定义为VARCHAR(45),login_time字段须建立B-TREE索引。
6、在app/tags.php中注册钩子映射,添加'login_success' => ['appcommonehaviorLoginLogBehavior'],确保框架能正确分发事件。
三、监听数据库执行事件捕获SQL级操作
该方法用于追踪底层数据变更行为,覆盖模型save/update/delete、查询构造器Db类操作及原生query调用,弥补中间件无法感知事务内多语句、软删除、批量更新等场景的盲区,提供与数据库实际执行完全一致的操作证据链。
1、在app/event.php中注册系统事件监听器:'db_query' => ['app\listener\DbQueryListener'], 'db_execute' => ['app\listener\DbExecuteListener']。
2、创建监听器类app/listener/DbQueryListener.php,在handle方法中接收$event和$params,提取$params['sql'](带占位符的原始语句)与$params['bind'](绑定参数数组)。
3、对$params['bind']执行敏感字段扫描,遍历键名,若命中['password', 'salt', 'id_card', 'bank_card']等关键词,则对该键值执行substr($value, 0, 4) . str_repeat('*', strlen($value) - 4)脱敏处理。
4、计算执行耗时:在DbQueryListener中记录microtime(true)为起始时间,在DbExecuteListener中再次获取并相减,得到毫秒级duration值。
5、组合日志条目,包含sql、bind(已脱敏)、duration、connection($params['connection'] ?? 'default')、trace_id(从thinkacadeRequest::header('x-trace-id', uniqid())获取,用于关联HTTP请求链路)。
6、调用file_put_contents()写入独立日志文件,路径为runtime/log/db_operation/'.date('Y-m-d').'.log,每行以date('Y-m-d H:i:s') . " " . json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "
"格式追加,并在写入前使用flock($fp, LOCK_EX)加锁防止并发丢行。



















