Files
datahub/backend/app/Middleware/RequestLogMiddleware.php
T

148 lines
4.5 KiB
PHP
Raw Normal View History

2026-03-17 11:40:07 +08:00
<?php
declare(strict_types=1);
namespace App\Middleware;
use App\Model\ApiRequestLog;
use App\Utils\Log;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Qbhy\HyperfAuth\AuthManager;
use Swoole\Coroutine;
class RequestLogMiddleware implements MiddlewareInterface
{
public function __construct(
protected AuthManager $auth
) {
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$start = microtime(true);
$response = $handler->handle($request);
$duration_ms = (int) ((microtime(true) - $start) * 1000);
// 在父协程中提取 user_id(子协程不继承 Swoole Context
$user_id = null;
try {
$user_id = $this->auth->guard('jwt')->user()?->getId();
} catch (\Throwable) {
// 未认证请求,user_id 保持 null
}
$method = $request->getMethod();
$path = $request->getUri()->getPath();
$status_code = $response->getStatusCode();
$ip = self::getClientIp($request);
$user_agent = $request->getHeaderLine('User-Agent') ?: null;
// GET/DELETE 请求不记录请求体
$request_body = null;
if (!in_array($method, ['GET', 'DELETE', 'HEAD', 'OPTIONS'], true)) {
$parsed_body = $request->getParsedBody();
if (is_array($parsed_body) && !empty($parsed_body)) {
$request_body = self::sanitizeBody($parsed_body);
}
}
$response_code = self::extractResponseCode($response);
// 异步协程写库,不阻塞响应返回
Coroutine::create(static function () use (
$user_id, $method, $path, $status_code, $ip,
$user_agent, $request_body, $response_code, $duration_ms
): void {
try {
ApiRequestLog::query()->create([
'user_id' => $user_id,
'method' => $method,
'path' => $path,
'status_code' => $status_code,
'ip' => $ip,
'user_agent' => $user_agent,
'request_body' => $request_body,
'response_code' => $response_code,
'duration_ms' => $duration_ms,
]);
} catch (\Throwable $e) {
Log::get()->error('RequestLogMiddleware: 写入请求日志失败', [
'error' => $e->getMessage(),
'path' => $path,
]);
}
});
return $response;
}
/**
* 递归替换敏感字段为 ***
*/
public static function sanitizeBody(array $body): array
{
$sensitive_keys = ['password', 'old_password', 'new_password', 'password_confirmation'];
foreach ($body as $key => $value) {
if (in_array($key, $sensitive_keys, true)) {
$body[$key] = '***';
} elseif (is_array($value)) {
$body[$key] = self::sanitizeBody($value);
}
}
return $body;
}
/**
* 从 JSON 响应体中提取 code 字段
*/
public static function extractResponseCode(ResponseInterface $response): ?int
{
$content_type = $response->getHeaderLine('Content-Type');
if (stripos($content_type, 'application/json') === false) {
return null;
}
try {
$body = (string) $response->getBody();
$data = json_decode($body, true);
if (is_array($data) && isset($data['code'])) {
return (int) $data['code'];
}
} catch (\Throwable) {
// 解析失败返回 null
}
return null;
}
/**
* 获取客户端真实 IP
*
* 优先级:X-Forwarded-For → X-Real-IP → ServerParams remote_addr
*/
public static function getClientIp(ServerRequestInterface $request): ?string
{
// X-Forwarded-For 可能包含多个 IP,取第一个
$forwarded_for = $request->getHeaderLine('X-Forwarded-For');
if ($forwarded_for !== '') {
$ips = explode(',', $forwarded_for);
return trim($ips[0]);
}
$real_ip = $request->getHeaderLine('X-Real-IP');
if ($real_ip !== '') {
return trim($real_ip);
}
$server_params = $request->getServerParams();
return $server_params['remote_addr'] ?? null;
}
}