tapi/app/http/middleware/ContextMiddleware.php
2025-08-29 15:38:12 +08:00

204 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();
/**
* @var Response $response
*/
$response = $next($request);
$response->header([
'R-Context-Id' => $request->contextId,
]);
/*
* 数据已加密
*/
if ($request->header('x-encrypted') == 'true') {
$encryptedData = $request->param('encryptedData','');
if($encryptedData) {
$jsonData = $this->decryptCryptoJSData($encryptedData);
$request->withPost($jsonData);
}
}
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): array
{// 1. Base64 解码加密数据
$encrypted = base64_decode($encryptedData);
// 2. 提取初始化向量 (IV) - 前16字节
if (strlen($encrypted) < 16) {
throw new ValidateException('加密数据太短无法提取IV');
}
$iv = substr($encrypted, 0, 16);
$ciphertext = substr($encrypted, 16);
// 3. 准备解密密钥
// CryptoJS 使用密钥派生函数,这里使用简单的 SHA256 哈希
$key = substr(hash('sha256', self::DATA_SECRET_KEY, true), 0, 32);
// 4. 使用 AES-256-CBC 解密
$decrypted = openssl_decrypt(
$ciphertext,
'AES-256-CBC',
$key,
OPENSSL_RAW_DATA,
$iv
);
if ($decrypted === false) {
throw new ValidateException('解密失败: ' . openssl_error_string());
}
// 5. 移除 PKCS#7 填充
$padding = ord($decrypted[strlen($decrypted) - 1]);
$decrypted = substr($decrypted, 0, -$padding);
// 6. 解析为 JSON
$data = json_decode($decrypted, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new ValidateException('JSON 解析失败: ' . json_last_error_msg());
}
return $data;
}
}