需结构化审计日志:一、中间件统一拦截请求记录轨迹;二、行为钩子捕获登录登出等关键事件;三、数据库监听器追踪sql变更;四、自定义审计日志驱动分离存储;五、异步队列写入防性能阻塞。

如果您在ThinkPHP6.x应用中需完整还原用户操作路径并捕获系统关键事件,但当前日志仅零散存在于runtime/log/或缺失上下文字段,则说明行为审计链路尚未结构化覆盖请求生命周期与业务节点。以下是解决此问题的步骤:
一、通过中间件统一拦截HTTP请求并记录轨迹
该方法在路由调度完成、控制器执行前捕获完整请求上下文,确保所有受控接口(除明确排除项外)均被覆盖,且能安全获取认证用户ID、真实客户端IP、设备指纹、请求参数及执行耗时,形成可串联的访问链路。
1、执行命令行生成中间件:php think make:middleware OperationTraceMiddleware。
2、在OperationTraceMiddleware的handle()方法开头调用microtime(true)记录起始时间戳,并通过$request->session()->get('user_id')获取登录态ID;若session未启动,则跳过用户字段写入。
3、构造设备指纹:拼接$request->header('User-Agent').$request->ip().$request->header('Accept-Language')后取md5值,存入device_fingerprint字段。
4、使用$request->rule()->getModule()、$request->rule()->getController()和$request->action()提取模块、控制器与动作名,组合为module/controller/action格式作为action字段值。
5、调用$request->except(['token', 'password', 'sign'])过滤敏感键后,再用$request->param()获取脱敏参数数组,并对过长字段如User-Agent执行mb_strcut($ua, 0, 200)截断。
6、构造日志数据数组,包含user_id(INT或NULL)、action(VARCHAR(100))、method(CHAR(10))、ip(VARCHAR(45))、device_fingerprint(CHAR(32))、params(JSON或TEXT)、duration_ms(FLOAT)、created_at(DATETIME),并使用json_encode($logData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)序列化。
7、插入数据库前检查是否匹配预设关键路由规则,例如Str::is('admin/user/*', $request->url()) || Str::is('api/v1/order/create', $request->url()),仅对匹配项写入。
二、利用行为钩子捕获登录、登出、权限变更等离散关键事件
该方法适用于由特定业务逻辑触发的非连续性事件,通过自定义钩子解耦日志代码与主流程,避免污染核心认证与权限逻辑,同时保证事件发生时机精准可控,且不依赖中间件生命周期。
1、在app/controller/Login.php登录成功分支末尾添加:Hook::listen('login_success', ['uid' => $user['id'], 'username' => $user['username'], 'ip' => request()->ip(), 'role_ids' => $user['role_ids']])。
2、在app/common/behavior/LoginLogBehavior.php中实现handle()方法,从参数中提取必要字段,构造最小审计数据,避免执行耗时操作(如查库、发HTTP请求)。
3、在登出逻辑(如app/controller/Logout.php)末尾调用Hook::listen('logout', ['uid' => session('user_id'), 'ip' => request()->ip()])。
4、在权限分配控制器(如app/controller/Admin/RoleAssign.php)中,于assign()方法成功提交后触发Hook::listen('permission_assigned', ['admin_id' => $adminId, 'target_uid' => $targetUid, 'permissions' => $perms])。
5、确保app/provider.php中已注册该行为类,例如返回数组包含'app\common\behavior\LoginLogBehavior',且类命名与路径严格符合框架约定。
三、通过数据库事件监听器追踪SQL执行细节与数据变更
该方法在模型层拦截INSERT、UPDATE、DELETE等写操作,捕获原始SQL、影响行数、关联主键及变更前后数据快照,用于回溯敏感数据修改过程,补充中间件与钩子无法覆盖的数据层行为。
1、创建监听器类app/listener/ModelChangeListener.php,监听think\model\event\AfterWrite、think\model\event\AfterDelete等事件。
2、在AfterWrite事件处理中,通过$event->model->name()获取表名,$event->model->getOriginData()获取变更前数据,$event->model->getData()获取变更后数据。
3、对password、api_key等敏感字段执行显式屏蔽,仅保留字段名与变更标识(如“已更新”、“已重置”)。
4、提取主键值(如$id = $event->model->getPkValue()),并拼接为record_id字段;若为新增操作,使用$event->model->getLastInsID()获取自增ID。
5、构造变更日志条目,含table_name、operation(INSERT/UPDATE/DELETE)、record_id、before_data(JSON)、after_data(JSON)、operator_id(从Auth或Session提取)、ip(request()->ip())、created_at,写入audit_model_changes表。
6、在config/database.php中启用事件监听,确保'trigger' => true配置已开启,并在对应模型类顶部声明use think\model\Pivot;或继承基类以支持事件触发。
四、新建专用审计日志驱动,强制分离存储路径与格式
该方法规避框架默认Log::write()混存于runtime/log/导致的检索困难、无用户上下文、不可结构化查询等问题,通过自定义驱动强制将审计日志写入独立目录与结构化格式,保障日志可溯源、可筛选、可聚合。
1、新建驱动类app/log/driver/AuditFile.php,继承think\log\driver\File,并重写save()方法,强制指定日志路径为runtime/audit/。
2、在save()方法中,对传入$data参数进行预处理:提取user_id(从Auth或Session)、ip(request()->ip())、action(如"admin.user.delete")、payload(关键业务字段JSON,如{"id":123,"reason":"test"})。
3、禁止记录原始$_POST或$request->param()全量数据,仅允许显式传入经业务层裁剪后的字段集合。
4、日志文件名按日期分片,格式为audit_Y-m-d.log,每行一条JSON记录,含trace_id(UUID v4生成)、timestamp(microtime(true))、level(固定为"audit")。
5、在config/log.php中注册该驱动:'audit' => ['type' => 'AuditFile', 'path' => runtime_path() . 'audit' . DIRECTORY_SEPARATOR]。
6、在中间件或钩子中调用Log::channel('audit')->info('', $logData)写入,而非使用默认通道。
五、异步写入日志以规避高并发性能阻塞
该方法防止同步IO操作(如file_put_contents或直接数据库插入)在高并发场景下拖慢接口响应,尤其当磁盘I/O延迟或日志量激增时,确保主业务流程不受日志写入影响。
1、安装think-queue扩展:composer require topthink/think-queue,并配置Redis或Database作为队列连接。
2、定义日志任务类app/job/WriteAuditLogJob.php,实现Fireable接口,在fire()方法中接收$logData数组并执行数据库插入或文件写入。
3、在OperationTraceMiddleware的handle()末尾,将日志数据推入队列:Queue::push(WriteAuditLogJob::class, $logData)。
4、在行为钩子处理类中,同样改用Queue::push()投递,禁止在handle()中执行任何同步落库或写文件操作。
5、启动消费者进程:php think queue:listen --queue=default,确保后台持续消费日志任务。
6、对失败任务设置最大重试次数(如3次)与失败回调,失败日志单独写入runtime/queue_failed/目录,便于人工干预。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











