You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

111 lines
3.5 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Http\Middleware;
use App\Models\AuditLog;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\File\UploadedFile as SymfonyUploadedFile;
use Symfony\Component\HttpFoundation\Response;
class AuditLogMiddleware
{
public function handle(Request $request, Closure $next): Response
{
/** @var Response $response */
$response = $next($request);
if (!$this->shouldLog($request)) {
return $response;
}
$user = $request->user();
try {
AuditLog::create([
'user_id' => $user?->id,
'username' => $user?->username ?: $user?->name,
'role' => $user?->role,
'method' => strtoupper($request->method()),
'path' => '/'.ltrim($request->path(), '/'),
'action' => strtoupper($request->method()).' '.$request->path(),
'status_code' => (int) $response->getStatusCode(),
'ip' => $request->ip(),
'user_agent' => substr((string) $request->userAgent(), 0, 500),
'request_payload' => $this->sanitizePayload($request->all()),
]);
} catch (\Throwable $e) {
Log::warning('audit_log_write_failed', [
'path' => $request->path(),
'message' => $e->getMessage(),
]);
}
return $response;
}
private function shouldLog(Request $request): bool
{
if ($request->isMethod('GET')) {
return false;
}
if ($request->is('api/audit-logs*')) {
return false;
}
// 任意 multipart 文件请求不参与审计(避免路径前缀不一致时仍序列化 UploadedFile
if ($request->files->count() > 0) {
return false;
}
// 仍按路由名兜底(无文件字段但走上传 URL 的极端情况)
if ($request->is('api/upload') || $request->is('api/h5/upload') || $request->is('*/api/upload') || $request->is('*/api/h5/upload')) {
return false;
}
return true;
}
private function sanitizePayload(array $payload): array
{
$sensitive = ['password', 'password_confirmation', 'token', 'access_token'];
$walk = function ($value) use (&$walk, $sensitive) {
if ($value instanceof SymfonyUploadedFile) {
return [
'_upload' => true,
'client_name' => $value->getClientOriginalName(),
'size' => $value->getSize(),
'mime' => $value->getClientMimeType(),
];
}
if (is_object($value)) {
if ($value instanceof \DateTimeInterface) {
return $value->format('c');
}
if ($value instanceof \JsonSerializable) {
return $walk($value->jsonSerialize());
}
return ['_object' => $value::class];
}
if (!is_array($value)) {
return $value;
}
$result = [];
foreach ($value as $k => $v) {
if (is_string($k) && in_array(strtolower($k), $sensitive, true)) {
$result[$k] = '***';
} else {
$result[$k] = $walk($v);
}
}
return $result;
};
return $walk($payload);
}
}