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;
|
|||
|
|
}
|
|||
|
|
}
|