tapi/app/http/middleware/ContextMiddleware.php
2025-08-29 16:03:03 +08:00

211 lines
7.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace app\http\middleware;
use app\entity\SysRequestRecord;
use Closure;
use think\{exception\ValidateException, file\UploadedFile, middleware, Request, Response};
class ContextMiddleware extends middleware
{
const string DATA_SECRET_KEY = "mySecretKey123!";
public function handle(Request $request, Closure $next): Response
{
/*
* 生成并绑定全局上下文标识ID
*/
$request->contextId = unique_str();
/*
* 数据已加密, 需要先做解密传递处理
*/
if ($request->header('x-encrypted') == 'true') {
$encryptedData = $request->param('encryptedData','');
if($encryptedData) {
$jsonInput = $this->decryptCryptoJSData($encryptedData);
if($jsonInput) {
return json(['code'=>500,'message'=>'E0.数据解密失败']);
}
$request->withInput($jsonInput);
}
}
/**
* @var Response $response
*/
$response = $next($request);
$response->header([
'R-Context-Id' => $request->contextId,
]);
return $response;
}
protected $noHttpLog = [
'GET@/adminapi/system/request-record'
];
/**
* 结束
* @param Response $response
* @return void
*/
public function end(Response $response): void
{
$request = $this->app->request;
$log = (array)$this->app->config->get('admin.httpLog');
$logOpen = $log['open'] ?? false;
if (!$logOpen || in_array($request->method() . '@' . $request->pathinfo(),$this->noHttpLog)) {
return;
}
$request_start_time = $request->time(true);
$request_end_time = microtime(true);
$running_time = ($request_end_time - $request_start_time) * 1000;
$openRecordStartTime = $log['open_start_time'] ?? 0;
$openRecordEndTime = $log['open_end_time'] ?? 0;
if ($openRecordStartTime && $openRecordEndTime) {
if ($request_end_time < $openRecordStartTime || $openRecordEndTime < $request_end_time) {
return;
}
}
$rules = array_merge([
'include_methods' => [],
'include_responses' => [],
'include_codes' => [],
'exclude_codes' => [],
], $log['recordRules'] ?? []);
// 请求类型(GET/POST/PUT/DELETE)
$request_method = $request->method();
if (!in_array($request_method, $rules['include_methods'])) {
return;
}
// 返回类型(JSON/HTML/XML)
$response_type = get_class($response);
if (!in_array($response_type, $rules['include_responses'])) {
return;
}
// 返回状态码(Http Status Code)
$response_code = $response->getCode();
if (in_array($response_type, $rules['exclude_codes'])) {
return;
}
if (count($rules['include_codes']) > 0) {
if (!in_array($response_type, $rules['include_codes'])) {
return;
}
}
$request_start_time = explode('.', $request_start_time);
$request_end_time = explode('.', $request_end_time);
$request_date = date('Y-m-d H:i:s', $request_start_time[0]);
$response_data = $response->getContent();
// 限制response_data记录长度
$response_max = $log['response_max'] ?? 0;
if ($response_max) {
$response_data = mb_substr($response_data, 0, $response_max, 'utf-8');
}
$request_body = $request->param();
try {
// 对文件类型的数据做处理
$request_file = $request->file();
if ($request_file && count($request_file) > 0) {
/**
* @var UploadedFile $file
*/
foreach ($request->file() as $key => $file) {
$request_body[$key] = [
'md5' => $file->md5(),
'name' => $file->getOriginalName(),
'size' => $file->getSize(),
];
}
}
$route = $request->rule()->getRoute();
$client = \app\Request::getClient();
(new SysRequestRecord)->save([
'context_id' => $request->contextId,
'client_id' => $client->id,
'client_name' => $client->name,
'client_version' => $client->version,
'request_date' => $request_date,
'request_time' => $request_date . '.' . ($request_start_time[1] ?? 0),
'request_method' => $request_method,
'request_end_time' => date('Y-m-d H:i:s', $request_end_time[0]) . '.' . ($request_end_time[1] ?? 0),
'request_headers' => json_encode($request->header(), JSON_UNESCAPED_UNICODE),
'request_body' => json_encode($request_body, JSON_UNESCAPED_UNICODE),
'request_path' => $request->url(),
'request_domain' => $request->domain(),
'response_type' => $response_type,
'response_code' => $response_code,
'response_data' => $response_data,
'running_time' => (int)$running_time,
'running_memory_limit' => ini_get('memory_limit'),
'running_memory_usage' => memory_get_peak_usage(),
'request_ip' => $request->ip(),
'rule_name' => $request->rule()->getName(),
'rule_route' => is_array($route) ? implode('@', $route) : @json_encode($route),
]);
} catch (\Throwable $e) {
$this->app->log->error("[ContextMiddleware::end] 日志写入失败->{$e->getMessage()}");
}
}
/**
* 解密 CryptoJS 加密的数据
*
* @param string $encryptedData Base64 编码的加密数据
* @return array 解密后的数据
* @throws ValidateException 解密失败时抛出异常
*/
private function decryptCryptoJSData(string $encryptedData): string
{
// Base64解码
$data = base64_decode($encryptedData);
// 检查Salted__头
if (!str_starts_with($data, "Salted__")) {
throw new ValidateException('Invalid format: missing "Salted__" header');
}
// 提取Salt (8字节)
$salt = substr($data, 8, 8);
$ct = substr($data, 16);
// 通过EVP_BytesToKey派生密钥和IV
$keyIv = $this->evpBytesToKey($salt);
$key = $keyIv['key'];
$iv = $keyIv['iv'];
// AES-256-CBC解密
return openssl_decrypt(
$ct,
'aes-256-cbc',
$key,
OPENSSL_RAW_DATA,
$iv
);
}
private function evpBytesToKey($salt): array
{
$password = self::DATA_SECRET_KEY;
$bytes = '';
$last = '';
// 生成48字节32字节key + 16字节IV
while(strlen($bytes) < 48) {
$last = md5($last . $password . $salt, true);
$bytes .= $last;
}
return [
'key' => substr($bytes, 0, 32),
'iv' => substr($bytes, 32, 16)
];
}
}