审计日志应只记录有业务语义的高风险变更事件,如user_login、user_password_change、data_delete、config_update及登录失败;每条日志须含user_id、action、target_id、status、timestamp五字段;日志路径须在Web根目录外,禁用file_put_contents,改用Monolog+StreamHandler并设独立channel;敏感数据须过滤掩码,文件权限设为640并用chattr+a防篡改;数据库审计表需存old_values和new_values JSON快照,索引建在(table_name, record_id, created_at)上。

审计日志该记录哪些操作,不是所有动作都值得记
只记有业务语义的高风险变更事件,比如 user_login、user_password_change、data_delete、config_update,还有登录失败——它可能是暴力破解的起点。别把 echo、模板渲染、HTTP状态码这类无关操作塞进去,否则日志体积会指数级膨胀,grep都难定位。
每条日志必须含五个字段:user_id(未登录时用 ip)、action、target_id(如被删的订单 ID)、status(success 或 failed)、timestamp。缺一不可,否则审计时无法还原操作上下文。
别直接用 file_put_contents 写审计日志
并发稍高就卡死:PHP 的 LOCK_EX 是阻塞锁,10 个请求同时写,9 个排队等,响应延迟飙升;更严重的是,如果日志路径在 Web 根目录下(比如 ./logs/audit.log),攻击者可能通过 ../logs/audit.log 直接下载日志,泄露管理员账号和操作痕迹。
正确做法:
立即学习“PHP免费学习笔记(深入)”;
- 日志路径必须放在 Web 根目录外,例如
/var/log/myapp/audit.log - 禁用
file_put_contents,改用Monolog+StreamHandler - 给审计日志单独打 channel,比如
$auditLogger = new Logger('audit'),避免和 debug/error 日志混在一起 - 注意
LineFormatter对数组的处理:默认把%context%转成字符串Array,关键字段(如旧值/新值)会丢失,得自定义 formatter 或提前序列化
敏感字段不能裸奔进日志内容
用户输入直接拼进日志行?危险。比如密码、token、身份证号,哪怕只是调试时临时加的 print_r($_POST),都可能落盘泄露。
实操要点:
- 写入前先过
filter_var($input, FILTER_SANITIZE_STRING)或至少htmlspecialchars() - 禁止把原始
$_POST、$_GET整体塞进context;要提取明确字段,且对敏感键(如password、api_key)做掩码处理,例如'password' => '***' - 日志文件权限设为
640,属主为www-data:adm,确保只有 Web 进程和运维组可读 - 用
chattr +a /var/log/myapp/audit.log锁定追加权限,防篡改;再配logrotate定期归档压缩
数据库审计表设计容易漏掉「值对比」能力
只记 user_id、action、table_name、record_id 不够。医疗、金融类系统尤其需要知道「改了什么」——比如患者诊断从「感冒」改成「肺炎」,这个语义差异常是合规审查核心。
必须存两份结构化快照:
-
old_values字段存 JSON,内容是变更前整行数据(用json_encode($originalRow)) -
new_values同理存变更后数据 - 字段类型选
TEXT而非VARCHAR,避免超长 JSON 截断 - 索引建在
(table_name, record_id, created_at)组合上,查某条记录历史变更才快
中间件自动捕获时,$this->getOriginalData($request) 得真去查 DB 原始值,不能只靠 request payload——用户可能绕过前端,直接调 API 提交伪造的「旧值」。



















