157 lines
5.0 KiB
PHP
157 lines
5.0 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Middleware;
|
||
|
||
use App\Models\ApiEndpointModel;
|
||
use App\Models\ApiOpLogModel;
|
||
use App\Service\common\UserAgentService;
|
||
use Closure;
|
||
use Firebase\JWT\JWT;
|
||
use Firebase\JWT\Key;
|
||
use Illuminate\Http\Request;
|
||
use Symfony\Component\HttpFoundation\Response;
|
||
|
||
/**
|
||
* API 操作日志中间件:响应结束后按接口注册表决定是否落库
|
||
*/
|
||
class ApiOpLogMiddleware
|
||
{
|
||
/**
|
||
* 透传请求;真正写日志在 terminate,避免拖慢接口响应
|
||
*/
|
||
public function handle(Request $request, Closure $next): Response
|
||
{
|
||
return $next($request);
|
||
}
|
||
|
||
/**
|
||
* 响应结束后写操作日志
|
||
* 匹配:去掉 /api/ 前缀的 url,且 method=0(ANY) 或等于请求方法,is_log=1、status=1
|
||
*/
|
||
public function terminate(Request $request, Response $response): void
|
||
{
|
||
try {
|
||
$path = $this->normalizePath($request->path());
|
||
if ($path === '') {
|
||
return;
|
||
}
|
||
$noInsert = config('nl.log.no_insert', []);
|
||
if (is_array($noInsert)) {
|
||
$full = 'api/' . $path;
|
||
if (in_array($path, $noInsert, true)
|
||
|| in_array($full, $noInsert, true)
|
||
|| in_array('/' . $full, $noInsert, true)
|
||
) {
|
||
return;
|
||
}
|
||
}
|
||
$methodCode = $this->mapMethod($request->method());
|
||
$endpoint = ApiEndpointModel::where('url', $path)
|
||
->where('deleted_at', 0)
|
||
->where('status', 1)
|
||
->where('is_log', 1)
|
||
->where(function ($q) use ($methodCode) {
|
||
$q->where('method', 0)->orWhere('method', $methodCode);
|
||
})
|
||
->orderByDesc('method')
|
||
->first();
|
||
if (!$endpoint) {
|
||
return;
|
||
}
|
||
$userId = $this->resolveUserId($request);
|
||
$body = $response->getContent();
|
||
$result = json_decode((string) $body, true);
|
||
if (!is_array($result)) {
|
||
$result = ['raw' => mb_substr((string) $body, 0, 2000)];
|
||
}
|
||
$code = $result['code'] ?? null;
|
||
$type = ((int) $code === 0) ? 0 : 1;
|
||
$ua = UserAgentService::getInstance();
|
||
ApiOpLogModel::insert([
|
||
'user_id' => $userId,
|
||
'url' => $path,
|
||
'method' => $methodCode,
|
||
'controller' => (string) ($endpoint->controller ?? ''),
|
||
'ip' => (string) get_ip(),
|
||
'param' => json_encode($this->collectParams($request), JSON_UNESCAPED_UNICODE),
|
||
'result' => json_encode($result, JSON_UNESCAPED_UNICODE),
|
||
'type' => $type,
|
||
'result_code' => (string) ($code ?? ''),
|
||
'platform_type' => 0,
|
||
'belong_id' => 0,
|
||
'user_type' => 0,
|
||
'equipment' => $ua->parseEquipment(),
|
||
'browser' => $ua->parseBrowser(),
|
||
'created_at' => time(),
|
||
]);
|
||
} catch (\Throwable $e) {
|
||
// 日志失败不影响主流程
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 规范化路径:去掉 api/ 前缀
|
||
*/
|
||
private function normalizePath(string $path): string
|
||
{
|
||
$path = trim($path, '/');
|
||
if (str_starts_with($path, 'api/')) {
|
||
$path = substr($path, 4);
|
||
}
|
||
return trim($path, '/');
|
||
}
|
||
|
||
/**
|
||
* HTTP 方法映射:GET=1 POST=2 其他=0
|
||
*/
|
||
private function mapMethod(string $method): int
|
||
{
|
||
return match (strtoupper($method)) {
|
||
'GET' => 1,
|
||
'POST' => 2,
|
||
default => 0,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 从 Bearer JWT 解析 user_id;失败返回 0(不抛鉴权异常)
|
||
*/
|
||
private function resolveUserId(Request $request): int
|
||
{
|
||
try {
|
||
$token = $request->bearerToken();
|
||
if (empty($token)) {
|
||
return 0;
|
||
}
|
||
$secret = (string) config('nl.jwt.secret', '');
|
||
if (strlen($secret) < 32) {
|
||
$secret = hash('sha256', $secret !== '' ? $secret : 'nl_admin_jwt_fallback_secret');
|
||
}
|
||
$decoded = JWT::decode($token, new Key($secret, 'HS256'));
|
||
return (int) ($decoded->data->id ?? 0);
|
||
} catch (\Throwable $e) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 收集请求参数并脱敏密码类字段
|
||
*
|
||
* @return array<string,mixed>
|
||
*/
|
||
private function collectParams(Request $request): array
|
||
{
|
||
$all = array_merge($request->query(), $request->request->all());
|
||
$maskKeys = [
|
||
'password', 'old_password', 'new_password', 'confirm_password',
|
||
'access_key', 'secret_key', 'api_key', 'token',
|
||
];
|
||
foreach ($maskKeys as $k) {
|
||
if (array_key_exists($k, $all) && $all[$k] !== '' && $all[$k] !== null) {
|
||
$all[$k] = '******';
|
||
}
|
||
}
|
||
return $all;
|
||
}
|
||
}
|