/audit/security_audit.log) * - 超过 20MB 自动轮转一次(保留一份 .old) * - 写失败静默(不影响业务),避免审计故障拖垮登录流程 * * 事件类型(event): * LOGIN_OK / LOGIN_FAIL / LOGIN_BLOCKED / PWD_CHANGE / PWD_CHANGE_FAIL * PWD_RESET / PWD_ADMIN_SET / LOGOUT * * 日志格式(每行一个 JSON 对象,便于 grep / 导入 SIEM): * {"ts":"...","ip":"...","user":"...","event":"...","detail":"..."} */ class Audit { public static function log(string $event, ?string $user = null, string $detail = '') : void { global $audit_log_file, $timezone; $file = $audit_log_file; if (empty($file)) { $file = dirname(__DIR__, 2) . '/audit/security_audit.log'; } // 确保日志目录存在(部署时可能尚未手工创建 audit/ 目录) $dir = dirname($file); if (!is_dir($dir)) { @mkdir($dir, 0775, true); } // 防审计文件无限增长:超过 20MB 轮转一次(保留一份 .old) if (is_file($file) && (filesize($file) > 20 * 1024 * 1024)) { @rename($file, $file . '.old'); } // 统一按 MRBS 配置时区记录时间戳(登录处理发生在 init_area() 之前, // 彼时 PHP 默认时区可能仍是 UTC,会导致同一日志两种时区混写) $old_tz = date_default_timezone_get(); if (!empty($timezone)) { @date_default_timezone_set($timezone); } $ts = date('c'); if (!empty($old_tz)) { @date_default_timezone_set($old_tz); } $record = array( 'ts' => $ts, 'ip' => $_SERVER['REMOTE_ADDR'] ?? '-', 'user' => $user ?? '-', 'event' => $event, 'detail' => $detail ); @file_put_contents($file, json_encode($record, JSON_UNESCAPED_UNICODE) . "\n", FILE_APPEND | LOCK_EX); } }