From 7a072e4becb97bbcba98549af92889a1f9b30ec1 Mon Sep 17 00:00:00 2001 From: u2nyakim Date: Fri, 29 Aug 2025 15:38:12 +0800 Subject: [PATCH] =?UTF-8?q?up.=20=E5=8F=8C=E5=90=91=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=8A=A0=E5=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/http/middleware/ContextMiddleware.php | 64 ++++++++++++++++++++++- route/api.php | 2 +- z_ele/package.json | 2 + z_ele/src/api/index.ts | 2 + z_ele/src/config/setting.ts | 3 ++ z_ele/src/utils/request.ts | 55 +++++++++++++++++-- 6 files changed, 122 insertions(+), 6 deletions(-) diff --git a/app/http/middleware/ContextMiddleware.php b/app/http/middleware/ContextMiddleware.php index 62814a1..dc92494 100644 --- a/app/http/middleware/ContextMiddleware.php +++ b/app/http/middleware/ContextMiddleware.php @@ -4,10 +4,12 @@ namespace app\http\middleware; use app\entity\SysRequestRecord; use Closure; -use think\{file\UploadedFile, middleware, Request, Response}; +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 { @@ -22,6 +24,16 @@ class ContextMiddleware extends middleware $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; } @@ -139,4 +151,54 @@ class ContextMiddleware extends middleware $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; + } } \ No newline at end of file diff --git a/route/api.php b/route/api.php index 3890a09..b92a844 100644 --- a/route/api.php +++ b/route/api.php @@ -160,7 +160,7 @@ Route::group("adminapi", function () { })->layer('admin') ->middleware([AllowCrossDomain::class], [ - 'Access-Control-Allow-Headers' => 'Client, Client-Version, Client-Id, Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With' + 'Access-Control-Allow-Headers' => 'Client, Client-Version, Client-Id, Authorization, X-Encrypted, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With' ])->middleware([ClientMiddleware::class]); // AllowCrossDomain 缺一个预检检查. //if($request->isOptions()) { diff --git a/z_ele/package.json b/z_ele/package.json index 0cb6703..75305bb 100644 --- a/z_ele/package.json +++ b/z_ele/package.json @@ -19,11 +19,13 @@ "@bytemd/plugin-gfm": "1.22.0", "@bytemd/plugin-highlight": "1.22.0", "@element-plus/icons-vue": "2.3.1", + "@types/crypto-js": "^4.2.2", "@vueuse/core": "13.3.0", "axios": "1.9.0", "bytemd": "1.22.0", "countup.js": "2.8.2", "cropperjs": "1.6.2", + "crypto-js": "^4.2.0", "dayjs": "1.11.13", "echarts": "5.6.0", "echarts-wordcloud": "2.1.0", diff --git a/z_ele/src/api/index.ts b/z_ele/src/api/index.ts index b9dbd7f..a5c9e2c 100644 --- a/z_ele/src/api/index.ts +++ b/z_ele/src/api/index.ts @@ -8,6 +8,8 @@ export interface ApiResult { message?: string; /** 返回数据 */ data?: T; + /** 后台做了数据加密->前端需要做对称解密 */ + encrypted?: string; } /** diff --git a/z_ele/src/config/setting.ts b/z_ele/src/config/setting.ts index 799f78f..f4a78c4 100644 --- a/z_ele/src/config/setting.ts +++ b/z_ele/src/config/setting.ts @@ -33,3 +33,6 @@ export const MAP_KEY = '006d995d433058322319fa797f2876f5'; /** EleAdminPlus授权码 */ export const LICENSE_CODE = import.meta.env.VITE_LICENSE; + + +export const DATA_SECRET_KEY = "mySecretKey123!" diff --git a/z_ele/src/utils/request.ts b/z_ele/src/utils/request.ts index 61b53b0..ec3bb5a 100644 --- a/z_ele/src/utils/request.ts +++ b/z_ele/src/utils/request.ts @@ -2,20 +2,55 @@ * axios实例 */ import axios from 'axios'; -import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios'; +import type { AxiosResponse, InternalAxiosRequestConfig, Method } from 'axios'; import { unref } from 'vue'; -import { API_BASE_URL, LAYOUT_PATH } from '@/config/setting'; +import { API_BASE_URL, LAYOUT_PATH, DATA_SECRET_KEY } from '@/config/setting'; import type { ApiResult } from '@/api'; import router from '@/router'; import { isWhiteList } from '@/router/routes'; import { getToken, setToken } from './token-util'; -import {logout, showLogoutConfirm, toURLSearch} from './common'; +import { logout, showLogoutConfirm, toURLSearch } from './common'; import {useClientStore} from "@/store/modules/client"; +import CryptoJS from 'crypto-js'; +interface SecureRequestConfig extends InternalAxiosRequestConfig { + /** 是否跳过加密处理(适用于文件上传等非文本数据) */ + skipEncryption?: boolean; +} +/** + * 加密数据 + * @param data + */ +export function encryptData(data: any): string { + const jsonString = JSON.stringify(data); + return CryptoJS.AES.encrypt(jsonString, DATA_SECRET_KEY).toString(); +} + +/** + * 解密数据 + * @param ciphertext + */ +export function decryptData(ciphertext: string): any { + const bytes = CryptoJS.AES.decrypt(ciphertext, DATA_SECRET_KEY); + const jsonString = bytes.toString(CryptoJS.enc.Utf8); + + if (!jsonString) { + throw new Error('Decryption failed: Invalid key or ciphertext'); + } + + return JSON.parse(jsonString); +} /** * 请求拦截处理 */ -export function requestInterceptor(config: InternalAxiosRequestConfig) { +export function requestInterceptor(config: SecureRequestConfig) { + const method = config.method?.toLowerCase() as Method; + // 是否加密 + const shouldEncrypt = ( + !config.skipEncryption && + config.data && + ['post', 'put', 'delete', 'patch'].includes(method)); + // 添加客户端信息到header const client = useClientStore(); if(config.headers) { @@ -23,6 +58,7 @@ export function requestInterceptor(config: InternalAxiosRequestConfig) { config.headers['Client-Id'] = client.clientId; config.headers['Client-Version'] = client.clientVersion; } + // 添加token到header const token = getToken(); if (token && config.headers) { @@ -33,6 +69,13 @@ export function requestInterceptor(config: InternalAxiosRequestConfig) { config.url = toURLSearch(config.params, config.url); config.params = {}; } + console.log("是否需要加密->", shouldEncrypt); + if (shouldEncrypt && !config.headers?.['X-Encrypted']) { + config.data = { + encryptedData: encryptData(config.data) + }; + config.headers['X-Encrypted'] = true; + } } /** @@ -68,6 +111,10 @@ const service = axios.create({ */ service.interceptors.response.use( (res: AxiosResponse>) => { + if (res.data?.encrypted) { + res.data = decryptData(res.data.encrypted); + } + const errorMessage = responseInterceptor(res); if (errorMessage) { return Promise.reject(new Error(errorMessage));