diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index 5c5188bec..547c8c5e7 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -47,10 +47,8 @@ "@vueuse/integrations": "catalog:", "ant-design-vue": "catalog:", "cropperjs": "catalog:", - "crypto-js": "catalog:", "dayjs": "catalog:", "highlight.js": "catalog:", - "jsencrypt": "catalog:", "pinia": "catalog:", "vue": "catalog:", "vue-dompurify-html": "catalog:", diff --git a/apps/web-antd/src/api/request.ts b/apps/web-antd/src/api/request.ts index d4e18d55d..2568def87 100644 --- a/apps/web-antd/src/api/request.ts +++ b/apps/web-antd/src/api/request.ts @@ -12,16 +12,17 @@ import { RequestClient, } from '@vben/request'; import { useAccessStore } from '@vben/stores'; +import { createApiEncrypt } from '@vben/utils'; import { message } from 'ant-design-vue'; import { useAuthStore } from '#/store'; -import { ApiEncrypt } from '#/utils/encrypt'; import { refreshTokenApi } from './core'; const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const tenantEnable = isTenantEnable(); +const apiEncrypt = createApiEncrypt(import.meta.env); function createRequestClient(baseURL: string, options?: RequestClientOptions) { const client = new RequestClient({ @@ -91,9 +92,9 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { try { // 加密请求数据 if (config.data) { - config.data = ApiEncrypt.encryptRequest(config.data); + config.data = apiEncrypt.encryptRequest(config.data); // 设置加密标识头 - config.headers[ApiEncrypt.getEncryptHeader()] = 'true'; + config.headers[apiEncrypt.getEncryptHeader()] = 'true'; } } catch (error) { console.error('请求数据加密失败:', error); @@ -108,14 +109,14 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { client.addResponseInterceptor({ fulfilled: (response) => { // 检查是否需要解密响应数据 - const encryptHeader = ApiEncrypt.getEncryptHeader(); + const encryptHeader = apiEncrypt.getEncryptHeader(); const isEncryptResponse = response.headers[encryptHeader] === 'true' || response.headers[encryptHeader.toLowerCase()] === 'true'; if (isEncryptResponse && typeof response.data === 'string') { try { // 解密响应数据 - response.data = ApiEncrypt.decryptResponse(response.data); + response.data = apiEncrypt.decryptResponse(response.data); } catch (error) { console.error('响应数据解密失败:', error); throw new Error(`响应数据解密失败: ${(error as Error).message}`); diff --git a/apps/web-ele/package.json b/apps/web-ele/package.json index 25c8b0a59..260f13b89 100644 --- a/apps/web-ele/package.json +++ b/apps/web-ele/package.json @@ -45,11 +45,9 @@ "@vben/utils": "workspace:*", "@vueuse/core": "catalog:", "cropperjs": "catalog:", - "crypto-js": "catalog:", "dayjs": "catalog:", "element-plus": "catalog:", "highlight.js": "catalog:", - "jsencrypt": "catalog:", "pinia": "catalog:", "vue": "catalog:", "vue-router": "catalog:" diff --git a/apps/web-ele/src/api/request.ts b/apps/web-ele/src/api/request.ts index 68633b721..97462d84a 100644 --- a/apps/web-ele/src/api/request.ts +++ b/apps/web-ele/src/api/request.ts @@ -12,16 +12,17 @@ import { RequestClient, } from '@vben/request'; import { useAccessStore } from '@vben/stores'; +import { createApiEncrypt } from '@vben/utils'; import { ElMessage } from 'element-plus'; import { useAuthStore } from '#/store'; -import { ApiEncrypt } from '#/utils/encrypt'; import { refreshTokenApi } from './core'; const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const tenantEnable = isTenantEnable(); +const apiEncrypt = createApiEncrypt(import.meta.env); function createRequestClient(baseURL: string, options?: RequestClientOptions) { const client = new RequestClient({ @@ -91,9 +92,9 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { try { // 加密请求数据 if (config.data) { - config.data = ApiEncrypt.encryptRequest(config.data); + config.data = apiEncrypt.encryptRequest(config.data); // 设置加密标识头 - config.headers[ApiEncrypt.getEncryptHeader()] = 'true'; + config.headers[apiEncrypt.getEncryptHeader()] = 'true'; } } catch (error) { console.error('请求数据加密失败:', error); @@ -108,14 +109,14 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { client.addResponseInterceptor({ fulfilled: (response) => { // 检查是否需要解密响应数据 - const encryptHeader = ApiEncrypt.getEncryptHeader(); + const encryptHeader = apiEncrypt.getEncryptHeader(); const isEncryptResponse = response.headers[encryptHeader] === 'true' || response.headers[encryptHeader.toLowerCase()] === 'true'; if (isEncryptResponse && typeof response.data === 'string') { try { // 解密响应数据 - response.data = ApiEncrypt.decryptResponse(response.data); + response.data = apiEncrypt.decryptResponse(response.data); } catch (error) { console.error('响应数据解密失败:', error); throw new Error(`响应数据解密失败: ${(error as Error).message}`); diff --git a/apps/web-ele/src/utils/encrypt.ts b/apps/web-ele/src/utils/encrypt.ts deleted file mode 100644 index 1b478e864..000000000 --- a/apps/web-ele/src/utils/encrypt.ts +++ /dev/null @@ -1,240 +0,0 @@ -import CryptoJS from 'crypto-js'; -import { JSEncrypt } from 'jsencrypt'; - -/** - * API 加解密工具类 - * 支持 AES 和 RSA 加密算法 - */ - -// 从环境变量获取配置 -const API_ENCRYPT_ENABLE = - import.meta.env.VITE_APP_API_ENCRYPT_ENABLE === 'true'; -const API_ENCRYPT_HEADER = - import.meta.env.VITE_APP_API_ENCRYPT_HEADER || 'X-Api-Encrypt'; -const API_ENCRYPT_ALGORITHM = - import.meta.env.VITE_APP_API_ENCRYPT_ALGORITHM || 'AES'; -const API_ENCRYPT_REQUEST_KEY = - import.meta.env.VITE_APP_API_ENCRYPT_REQUEST_KEY || ''; // AES密钥 或 RSA公钥 -const API_ENCRYPT_RESPONSE_KEY = - import.meta.env.VITE_APP_API_ENCRYPT_RESPONSE_KEY || ''; // AES密钥 或 RSA私钥 - -/** - * AES 加密工具类 - */ -export const AES = { - /** - * AES 加密 - * @param data 要加密的数据 - * @param key 加密密钥 - * @returns 加密后的字符串 - */ - encrypt(data: string, key: string): string { - try { - if (!key) { - throw new Error('AES 加密密钥不能为空'); - } - if (key.length !== 32) { - throw new Error( - `AES 加密密钥长度必须为 32 位,当前长度: ${key.length}`, - ); - } - - const keyUtf8 = CryptoJS.enc.Utf8.parse(key); - const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, { - mode: CryptoJS.mode.ECB, - padding: CryptoJS.pad.Pkcs7, - }); - return encrypted.toString(); - } catch (error) { - console.error('AES 加密失败:', error); - throw error; - } - }, - - /** - * AES 解密 - * @param encryptedData 加密的数据 - * @param key 解密密钥 - * @returns 解密后的字符串 - */ - decrypt(encryptedData: string, key: string): string { - try { - if (!key) { - throw new Error('AES 解密密钥不能为空'); - } - if (key.length !== 32) { - throw new Error( - `AES 解密密钥长度必须为 32 位,当前长度: ${key.length}`, - ); - } - if (!encryptedData) { - throw new Error('AES 解密数据不能为空'); - } - - const keyUtf8 = CryptoJS.enc.Utf8.parse(key); - const decrypted = CryptoJS.AES.decrypt(encryptedData, keyUtf8, { - mode: CryptoJS.mode.ECB, - padding: CryptoJS.pad.Pkcs7, - }); - const result = decrypted.toString(CryptoJS.enc.Utf8); - if (!result) { - throw new Error('AES 解密结果为空,可能是密钥错误或数据损坏'); - } - return result; - } catch (error) { - console.error('AES 解密失败:', error); - throw error; - } - }, -}; - -/** - * RSA 加密工具类 - */ -export const RSA = { - /** - * RSA 加密 - * @param data 要加密的数据 - * @param publicKey 公钥(必需) - * @returns 加密后的字符串 - */ - encrypt(data: string, publicKey: string): false | string { - try { - if (!publicKey) { - throw new Error('RSA 公钥不能为空'); - } - - const encryptor = new JSEncrypt(); - encryptor.setPublicKey(publicKey); - const result = encryptor.encrypt(data); - if (result === false) { - throw new Error('RSA 加密失败,可能是公钥格式错误或数据过长'); - } - return result; - } catch (error) { - console.error('RSA 加密失败:', error); - throw error; - } - }, - - /** - * RSA 解密 - * @param encryptedData 加密的数据 - * @param privateKey 私钥(必需) - * @returns 解密后的字符串 - */ - decrypt(encryptedData: string, privateKey: string): false | string { - try { - if (!privateKey) { - throw new Error('RSA 私钥不能为空'); - } - if (!encryptedData) { - throw new Error('RSA 解密数据不能为空'); - } - - const encryptor = new JSEncrypt(); - encryptor.setPrivateKey(privateKey); - const result = encryptor.decrypt(encryptedData); - if (result === false) { - throw new Error('RSA 解密失败,可能是私钥错误或数据损坏'); - } - return result; - } catch (error) { - console.error('RSA 解密失败:', error); - throw error; - } - }, -}; - -/** - * API 加解密主类 - */ -export const ApiEncrypt = { - /** - * 获取加密头名称 - */ - getEncryptHeader(): string { - return API_ENCRYPT_HEADER; - }, - - /** - * 加密请求数据 - * @param data 要加密的数据 - * @returns 加密后的数据 - */ - encryptRequest(data: any): string { - if (!API_ENCRYPT_ENABLE) { - return data; - } - - try { - const jsonData = typeof data === 'string' ? data : JSON.stringify(data); - - if (API_ENCRYPT_ALGORITHM.toUpperCase() === 'AES') { - if (!API_ENCRYPT_REQUEST_KEY) { - throw new Error('AES 请求加密密钥未配置'); - } - return AES.encrypt(jsonData, API_ENCRYPT_REQUEST_KEY); - } else if (API_ENCRYPT_ALGORITHM.toUpperCase() === 'RSA') { - if (!API_ENCRYPT_REQUEST_KEY) { - throw new Error('RSA 公钥未配置'); - } - const result = RSA.encrypt(jsonData, API_ENCRYPT_REQUEST_KEY); - if (result === false) { - throw new Error('RSA 加密失败'); - } - return result; - } else { - throw new Error(`不支持的加密算法: ${API_ENCRYPT_ALGORITHM}`); - } - } catch (error) { - console.error('请求数据加密失败:', error); - throw error; - } - }, - - /** - * 解密响应数据 - * @param encryptedData 加密的响应数据 - * @returns 解密后的数据 - */ - decryptResponse(encryptedData: string): any { - if (!API_ENCRYPT_ENABLE) { - return encryptedData; - } - - try { - let decryptedData: false | string = ''; - if (API_ENCRYPT_ALGORITHM.toUpperCase() === 'AES') { - if (!API_ENCRYPT_RESPONSE_KEY) { - throw new Error('AES 响应解密密钥未配置'); - } - decryptedData = AES.decrypt(encryptedData, API_ENCRYPT_RESPONSE_KEY); - } else if (API_ENCRYPT_ALGORITHM.toUpperCase() === 'RSA') { - if (!API_ENCRYPT_RESPONSE_KEY) { - throw new Error('RSA 私钥未配置'); - } - decryptedData = RSA.decrypt(encryptedData, API_ENCRYPT_RESPONSE_KEY); - if (decryptedData === false) { - throw new Error('RSA 解密失败'); - } - } else { - throw new Error(`不支持的解密算法: ${API_ENCRYPT_ALGORITHM}`); - } - - if (!decryptedData) { - throw new Error('解密结果为空'); - } - - // 尝试解析为 JSON,如果失败则返回原字符串 - try { - return JSON.parse(decryptedData); - } catch { - return decryptedData; - } - } catch (error) { - console.error('响应数据解密失败:', error); - throw error; - } - }, -}; diff --git a/packages/@core/base/shared/package.json b/packages/@core/base/shared/package.json index 6599c54c5..528c0d725 100644 --- a/packages/@core/base/shared/package.json +++ b/packages/@core/base/shared/package.json @@ -83,8 +83,10 @@ "@tanstack/vue-store": "catalog:", "@vue/shared": "catalog:", "clsx": "catalog:", + "crypto-js": "catalog:", "dayjs": "catalog:", "defu": "catalog:", + "jsencrypt": "catalog:", "lodash.clonedeep": "catalog:", "lodash.get": "catalog:", "lodash.isequal": "catalog:", diff --git a/apps/web-antd/src/utils/encrypt.ts b/packages/@core/base/shared/src/utils/encrypt.ts similarity index 71% rename from apps/web-antd/src/utils/encrypt.ts rename to packages/@core/base/shared/src/utils/encrypt.ts index 1b478e864..dd58cca48 100644 --- a/apps/web-antd/src/utils/encrypt.ts +++ b/packages/@core/base/shared/src/utils/encrypt.ts @@ -6,18 +6,6 @@ import { JSEncrypt } from 'jsencrypt'; * 支持 AES 和 RSA 加密算法 */ -// 从环境变量获取配置 -const API_ENCRYPT_ENABLE = - import.meta.env.VITE_APP_API_ENCRYPT_ENABLE === 'true'; -const API_ENCRYPT_HEADER = - import.meta.env.VITE_APP_API_ENCRYPT_HEADER || 'X-Api-Encrypt'; -const API_ENCRYPT_ALGORITHM = - import.meta.env.VITE_APP_API_ENCRYPT_ALGORITHM || 'AES'; -const API_ENCRYPT_REQUEST_KEY = - import.meta.env.VITE_APP_API_ENCRYPT_REQUEST_KEY || ''; // AES密钥 或 RSA公钥 -const API_ENCRYPT_RESPONSE_KEY = - import.meta.env.VITE_APP_API_ENCRYPT_RESPONSE_KEY || ''; // AES密钥 或 RSA私钥 - /** * AES 加密工具类 */ @@ -146,52 +134,31 @@ export const RSA = { }, }; +/** + * API 加解密配置接口 + */ +export interface ApiEncryptConfig { + /** 加密算法 */ + algorithm: 'AES' | 'RSA'; + /** 是否启用加解密 */ + enable: boolean; + /** 加密头名称 */ + header: string; + /** 请求加密密钥(AES密钥或RSA公钥) */ + requestKey: string; + /** 响应解密密钥(AES密钥或RSA私钥) */ + responseKey: string; +} + /** * API 加解密主类 */ -export const ApiEncrypt = { - /** - * 获取加密头名称 - */ - getEncryptHeader(): string { - return API_ENCRYPT_HEADER; - }, +export class ApiEncrypt { + private config: ApiEncryptConfig; - /** - * 加密请求数据 - * @param data 要加密的数据 - * @returns 加密后的数据 - */ - encryptRequest(data: any): string { - if (!API_ENCRYPT_ENABLE) { - return data; - } - - try { - const jsonData = typeof data === 'string' ? data : JSON.stringify(data); - - if (API_ENCRYPT_ALGORITHM.toUpperCase() === 'AES') { - if (!API_ENCRYPT_REQUEST_KEY) { - throw new Error('AES 请求加密密钥未配置'); - } - return AES.encrypt(jsonData, API_ENCRYPT_REQUEST_KEY); - } else if (API_ENCRYPT_ALGORITHM.toUpperCase() === 'RSA') { - if (!API_ENCRYPT_REQUEST_KEY) { - throw new Error('RSA 公钥未配置'); - } - const result = RSA.encrypt(jsonData, API_ENCRYPT_REQUEST_KEY); - if (result === false) { - throw new Error('RSA 加密失败'); - } - return result; - } else { - throw new Error(`不支持的加密算法: ${API_ENCRYPT_ALGORITHM}`); - } - } catch (error) { - console.error('请求数据加密失败:', error); - throw error; - } - }, + constructor(config: ApiEncryptConfig) { + this.config = config; + } /** * 解密响应数据 @@ -199,27 +166,27 @@ export const ApiEncrypt = { * @returns 解密后的数据 */ decryptResponse(encryptedData: string): any { - if (!API_ENCRYPT_ENABLE) { + if (!this.config.enable) { return encryptedData; } try { let decryptedData: false | string = ''; - if (API_ENCRYPT_ALGORITHM.toUpperCase() === 'AES') { - if (!API_ENCRYPT_RESPONSE_KEY) { + if (this.config.algorithm.toUpperCase() === 'AES') { + if (!this.config.responseKey) { throw new Error('AES 响应解密密钥未配置'); } - decryptedData = AES.decrypt(encryptedData, API_ENCRYPT_RESPONSE_KEY); - } else if (API_ENCRYPT_ALGORITHM.toUpperCase() === 'RSA') { - if (!API_ENCRYPT_RESPONSE_KEY) { + decryptedData = AES.decrypt(encryptedData, this.config.responseKey); + } else if (this.config.algorithm.toUpperCase() === 'RSA') { + if (!this.config.responseKey) { throw new Error('RSA 私钥未配置'); } - decryptedData = RSA.decrypt(encryptedData, API_ENCRYPT_RESPONSE_KEY); + decryptedData = RSA.decrypt(encryptedData, this.config.responseKey); if (decryptedData === false) { throw new Error('RSA 解密失败'); } } else { - throw new Error(`不支持的解密算法: ${API_ENCRYPT_ALGORITHM}`); + throw new Error(`不支持的解密算法: ${this.config.algorithm}`); } if (!decryptedData) { @@ -236,5 +203,65 @@ export const ApiEncrypt = { console.error('响应数据解密失败:', error); throw error; } - }, -}; + } + + /** + * 加密请求数据 + * @param data 要加密的数据 + * @returns 加密后的数据 + */ + encryptRequest(data: any): string { + if (!this.config.enable) { + return data; + } + + try { + const jsonData = typeof data === 'string' ? data : JSON.stringify(data); + + if (this.config.algorithm.toUpperCase() === 'AES') { + if (!this.config.requestKey) { + throw new Error('AES 请求加密密钥未配置'); + } + return AES.encrypt(jsonData, this.config.requestKey); + } else if (this.config.algorithm.toUpperCase() === 'RSA') { + if (!this.config.requestKey) { + throw new Error('RSA 公钥未配置'); + } + const result = RSA.encrypt(jsonData, this.config.requestKey); + if (result === false) { + throw new Error('RSA 加密失败'); + } + return result; + } else { + throw new Error(`不支持的加密算法: ${this.config.algorithm}`); + } + } catch (error) { + console.error('请求数据加密失败:', error); + throw error; + } + } + + /** + * 获取加密头名称 + */ + getEncryptHeader(): string { + return this.config.header; + } +} + +/** + * 创建基于环境变量的 API 加解密实例 + * @param env 环境变量对象 + * @returns ApiEncrypt 实例 + */ +export function createApiEncrypt(env: Record): ApiEncrypt { + const config: ApiEncryptConfig = { + enable: env.VITE_APP_API_ENCRYPT_ENABLE === 'true', + header: env.VITE_APP_API_ENCRYPT_HEADER || 'X-Api-Encrypt', + algorithm: env.VITE_APP_API_ENCRYPT_ALGORITHM || 'AES', + requestKey: env.VITE_APP_API_ENCRYPT_REQUEST_KEY || '', + responseKey: env.VITE_APP_API_ENCRYPT_RESPONSE_KEY || '', + }; + + return new ApiEncrypt(config); +} diff --git a/packages/@core/base/shared/src/utils/index.ts b/packages/@core/base/shared/src/utils/index.ts index ec6ac8346..65aadb015 100644 --- a/packages/@core/base/shared/src/utils/index.ts +++ b/packages/@core/base/shared/src/utils/index.ts @@ -3,6 +3,7 @@ export * from './date'; export * from './diff'; export * from './dom'; export * from './download'; +export * from './encrypt'; export * from './formatNumber'; export * from './inference'; export * from './letter'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 735c64ac5..557660a77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -776,18 +776,12 @@ importers: cropperjs: specifier: 'catalog:' version: 1.6.2 - crypto-js: - specifier: 'catalog:' - version: 4.2.0 dayjs: specifier: 'catalog:' version: 1.11.13 highlight.js: specifier: 'catalog:' version: 11.11.1 - jsencrypt: - specifier: 'catalog:' - version: 3.5.4 pinia: specifier: ^3.0.3 version: 3.0.3(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)) @@ -867,9 +861,6 @@ importers: cropperjs: specifier: 'catalog:' version: 1.6.2 - crypto-js: - specifier: 'catalog:' - version: 4.2.0 dayjs: specifier: 'catalog:' version: 1.11.13 @@ -879,9 +870,6 @@ importers: highlight.js: specifier: 'catalog:' version: 11.11.1 - jsencrypt: - specifier: 'catalog:' - version: 3.5.4 pinia: specifier: ^3.0.3 version: 3.0.3(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)) @@ -1377,12 +1365,18 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + crypto-js: + specifier: 'catalog:' + version: 4.2.0 dayjs: specifier: 'catalog:' version: 1.11.13 defu: specifier: 'catalog:' version: 6.1.4 + jsencrypt: + specifier: 'catalog:' + version: 3.5.4 lodash.clonedeep: specifier: 'catalog:' version: 4.5.0