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

3 days ago
<?php
namespace App\Http\Middleware;
use App\Models\AuditLog;
use Closure;
use Illuminate\Http\Request;
3 days ago
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\File\UploadedFile as SymfonyUploadedFile;
3 days ago
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();
3 days ago
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(),
]);
}
3 days ago
return $response;
}
private function shouldLog(Request $request): bool
{
if ($request->isMethod('GET')) {
return false;
}
if ($request->is('api/audit-logs*')) {
return false;
}
3 days ago
// 任意 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')) {
3 days ago
return false;
}
3 days ago
return true;
}
private function sanitizePayload(array $payload): array
{
$sensitive = ['password', 'password_confirmation', 'token', 'access_token'];
$walk = function ($value) use (&$walk, $sensitive) {
3 days ago
if ($value instanceof SymfonyUploadedFile) {
3 days ago
return [
'_upload' => true,
'client_name' => $value->getClientOriginalName(),
'size' => $value->getSize(),
'mime' => $value->getClientMimeType(),
];
}
3 days ago
if (is_object($value)) {
if ($value instanceof \DateTimeInterface) {
return $value->format('c');
}
if ($value instanceof \JsonSerializable) {
return $walk($value->jsonSerialize());
}
return ['_object' => $value::class];
}
3 days ago
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);
}
}
3 days ago
3 days ago
return $result;
};
return $walk($payload);
}
}