up. 双向数据加密

This commit is contained in:
u2nyakim 2025-08-29 15:38:12 +08:00
parent 4bfa97e4fd
commit 7a072e4bec
6 changed files with 122 additions and 6 deletions

View File

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

View File

@ -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()) {

View File

@ -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",

View File

@ -8,6 +8,8 @@ export interface ApiResult<T> {
message?: string;
/** 返回数据 */
data?: T;
/** 后台做了数据加密->前端需要做对称解密 */
encrypted?: string;
}
/**

View File

@ -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!"

View File

@ -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 {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<any>) {
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<any>) {
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<any>) {
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<ApiResult<unknown>>) => {
if (res.data?.encrypted) {
res.data = decryptData(res.data.encrypted);
}
const errorMessage = responseInterceptor(res);
if (errorMessage) {
return Promise.reject(new Error(errorMessage));