up. 双向数据加密
This commit is contained in:
parent
4bfa97e4fd
commit
7a072e4bec
@ -4,10 +4,12 @@ namespace app\http\middleware;
|
|||||||
|
|
||||||
use app\entity\SysRequestRecord;
|
use app\entity\SysRequestRecord;
|
||||||
use Closure;
|
use Closure;
|
||||||
use think\{file\UploadedFile, middleware, Request, Response};
|
use think\{exception\ValidateException, file\UploadedFile, middleware, Request, Response};
|
||||||
|
|
||||||
class ContextMiddleware extends middleware
|
class ContextMiddleware extends middleware
|
||||||
{
|
{
|
||||||
|
const string DATA_SECRET_KEY = "mySecretKey123!";
|
||||||
|
|
||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
|
|
||||||
@ -22,6 +24,16 @@ class ContextMiddleware extends middleware
|
|||||||
$response->header([
|
$response->header([
|
||||||
'R-Context-Id' => $request->contextId,
|
'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;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,4 +151,54 @@ class ContextMiddleware extends middleware
|
|||||||
$this->app->log->error("[ContextMiddleware::end] 日志写入失败->{$e->getMessage()}");
|
$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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -160,7 +160,7 @@ Route::group("adminapi", function () {
|
|||||||
|
|
||||||
})->layer('admin')
|
})->layer('admin')
|
||||||
->middleware([AllowCrossDomain::class], [
|
->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]);
|
])->middleware([ClientMiddleware::class]);
|
||||||
// AllowCrossDomain 缺一个预检检查.
|
// AllowCrossDomain 缺一个预检检查.
|
||||||
//if($request->isOptions()) {
|
//if($request->isOptions()) {
|
||||||
|
|||||||
@ -19,11 +19,13 @@
|
|||||||
"@bytemd/plugin-gfm": "1.22.0",
|
"@bytemd/plugin-gfm": "1.22.0",
|
||||||
"@bytemd/plugin-highlight": "1.22.0",
|
"@bytemd/plugin-highlight": "1.22.0",
|
||||||
"@element-plus/icons-vue": "2.3.1",
|
"@element-plus/icons-vue": "2.3.1",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@vueuse/core": "13.3.0",
|
"@vueuse/core": "13.3.0",
|
||||||
"axios": "1.9.0",
|
"axios": "1.9.0",
|
||||||
"bytemd": "1.22.0",
|
"bytemd": "1.22.0",
|
||||||
"countup.js": "2.8.2",
|
"countup.js": "2.8.2",
|
||||||
"cropperjs": "1.6.2",
|
"cropperjs": "1.6.2",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.13",
|
||||||
"echarts": "5.6.0",
|
"echarts": "5.6.0",
|
||||||
"echarts-wordcloud": "2.1.0",
|
"echarts-wordcloud": "2.1.0",
|
||||||
|
|||||||
@ -8,6 +8,8 @@ export interface ApiResult<T> {
|
|||||||
message?: string;
|
message?: string;
|
||||||
/** 返回数据 */
|
/** 返回数据 */
|
||||||
data?: T;
|
data?: T;
|
||||||
|
/** 后台做了数据加密->前端需要做对称解密 */
|
||||||
|
encrypted?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -33,3 +33,6 @@ export const MAP_KEY = '006d995d433058322319fa797f2876f5';
|
|||||||
|
|
||||||
/** EleAdminPlus授权码 */
|
/** EleAdminPlus授权码 */
|
||||||
export const LICENSE_CODE = import.meta.env.VITE_LICENSE;
|
export const LICENSE_CODE = import.meta.env.VITE_LICENSE;
|
||||||
|
|
||||||
|
|
||||||
|
export const DATA_SECRET_KEY = "mySecretKey123!"
|
||||||
|
|||||||
@ -2,20 +2,55 @@
|
|||||||
* axios实例
|
* axios实例
|
||||||
*/
|
*/
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
import type { AxiosResponse, InternalAxiosRequestConfig, Method } from 'axios';
|
||||||
import { unref } from 'vue';
|
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 type { ApiResult } from '@/api';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import { isWhiteList } from '@/router/routes';
|
import { isWhiteList } from '@/router/routes';
|
||||||
import { getToken, setToken } from './token-util';
|
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 {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
|
// 添加客户端信息到header
|
||||||
const client = useClientStore();
|
const client = useClientStore();
|
||||||
if(config.headers) {
|
if(config.headers) {
|
||||||
@ -23,6 +58,7 @@ export function requestInterceptor(config: InternalAxiosRequestConfig<any>) {
|
|||||||
config.headers['Client-Id'] = client.clientId;
|
config.headers['Client-Id'] = client.clientId;
|
||||||
config.headers['Client-Version'] = client.clientVersion;
|
config.headers['Client-Version'] = client.clientVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加token到header
|
// 添加token到header
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (token && config.headers) {
|
if (token && config.headers) {
|
||||||
@ -33,6 +69,13 @@ export function requestInterceptor(config: InternalAxiosRequestConfig<any>) {
|
|||||||
config.url = toURLSearch(config.params, config.url);
|
config.url = toURLSearch(config.params, config.url);
|
||||||
config.params = {};
|
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(
|
service.interceptors.response.use(
|
||||||
(res: AxiosResponse<ApiResult<unknown>>) => {
|
(res: AxiosResponse<ApiResult<unknown>>) => {
|
||||||
|
if (res.data?.encrypted) {
|
||||||
|
res.data = decryptData(res.data.encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
const errorMessage = responseInterceptor(res);
|
const errorMessage = responseInterceptor(res);
|
||||||
if (errorMessage) {
|
if (errorMessage) {
|
||||||
return Promise.reject(new Error(errorMessage));
|
return Promise.reject(new Error(errorMessage));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user