diff --git a/apps/web-antd/.env b/apps/web-antd/.env index 6b960186e..778a9fa1f 100644 --- a/apps/web-antd/.env +++ b/apps/web-antd/.env @@ -24,3 +24,12 @@ VITE_APP_BAIDU_CODE = e98f2eab6ceb8688bc6d8fc5332ff093 # GoView域名 VITE_GOVIEW_URL='http://127.0.0.1:3000' + +# API 加解密 +VITE_APP_API_ENCRYPT_ENABLE = true +VITE_APP_API_ENCRYPT_HEADER = X-Api-Encrypt +VITE_APP_API_ENCRYPT_ALGORITHM = AES +VITE_APP_API_ENCRYPT_REQUEST_KEY = 52549111389893486934626385991395 +VITE_APP_API_ENCRYPT_RESPONSE_KEY = 96103715984234343991809655248883 +# VITE_APP_API_ENCRYPT_REQUEST_KEY = MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB +# VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ== diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index a74ce8d0a..5c5188bec 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -50,6 +50,7 @@ "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/core/auth.ts b/apps/web-antd/src/api/core/auth.ts index ccb6da340..3b80a5789 100644 --- a/apps/web-antd/src/api/core/auth.ts +++ b/apps/web-antd/src/api/core/auth.ts @@ -64,7 +64,11 @@ export namespace AuthApi { /** 登录 */ export async function loginApi(data: AuthApi.LoginParams) { - return requestClient.post('/system/auth/login', data); + return requestClient.post('/system/auth/login', data, { + headers: { + isEncrypt: true, + }, + }); } /** 刷新 accessToken */ diff --git a/apps/web-antd/src/api/request.ts b/apps/web-antd/src/api/request.ts index 7f78987b4..d4e18d55d 100644 --- a/apps/web-antd/src/api/request.ts +++ b/apps/web-antd/src/api/request.ts @@ -16,6 +16,7 @@ import { useAccessStore } from '@vben/stores'; import { message } from 'ant-design-vue'; import { useAuthStore } from '#/store'; +import { ApiEncrypt } from '#/utils/encrypt'; import { refreshTokenApi } from './core'; @@ -84,10 +85,46 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { config.headers['visit-tenant-id'] = tenantEnable ? accessStore.visitTenantId : undefined; + + // 是否 API 加密 + if ((config.headers || {}).isEncrypt) { + try { + // 加密请求数据 + if (config.data) { + config.data = ApiEncrypt.encryptRequest(config.data); + // 设置加密标识头 + config.headers[ApiEncrypt.getEncryptHeader()] = 'true'; + } + } catch (error) { + console.error('请求数据加密失败:', error); + throw error; + } + } return config; }, }); + // API 解密响应拦截器 + client.addResponseInterceptor({ + fulfilled: (response) => { + // 检查是否需要解密响应数据 + 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); + } catch (error) { + console.error('响应数据解密失败:', error); + throw new Error(`响应数据解密失败: ${(error as Error).message}`); + } + } + return response; + }, + }); + // 处理返回的响应数据格式 client.addResponseInterceptor( defaultResponseInterceptor({ diff --git a/apps/web-antd/src/utils/encrypt.ts b/apps/web-antd/src/utils/encrypt.ts new file mode 100644 index 000000000..1b478e864 --- /dev/null +++ b/apps/web-antd/src/utils/encrypt.ts @@ -0,0 +1,240 @@ +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/apps/web-ele/.env b/apps/web-ele/.env index f72f25d44..eca5ebda5 100644 --- a/apps/web-ele/.env +++ b/apps/web-ele/.env @@ -24,3 +24,12 @@ VITE_APP_BAIDU_CODE = b79d8f49e2d38b26503b92810b740f45 # GoView域名 VITE_GOVIEW_URL='http://127.0.0.1:3000' + +# API 加解密 +VITE_APP_API_ENCRYPT_ENABLE = true +VITE_APP_API_ENCRYPT_HEADER = X-Api-Encrypt +VITE_APP_API_ENCRYPT_ALGORITHM = AES +VITE_APP_API_ENCRYPT_REQUEST_KEY = 52549111389893486934626385991395 +VITE_APP_API_ENCRYPT_RESPONSE_KEY = 96103715984234343991809655248883 +# VITE_APP_API_ENCRYPT_REQUEST_KEY = MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB +# VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ== diff --git a/apps/web-ele/package.json b/apps/web-ele/package.json index 43a43dac7..25c8b0a59 100644 --- a/apps/web-ele/package.json +++ b/apps/web-ele/package.json @@ -49,6 +49,7 @@ "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/core/auth.ts b/apps/web-ele/src/api/core/auth.ts index ccb6da340..3b80a5789 100644 --- a/apps/web-ele/src/api/core/auth.ts +++ b/apps/web-ele/src/api/core/auth.ts @@ -64,7 +64,11 @@ export namespace AuthApi { /** 登录 */ export async function loginApi(data: AuthApi.LoginParams) { - return requestClient.post('/system/auth/login', data); + return requestClient.post('/system/auth/login', data, { + headers: { + isEncrypt: true, + }, + }); } /** 刷新 accessToken */ diff --git a/apps/web-ele/src/api/request.ts b/apps/web-ele/src/api/request.ts index af7d95ab9..68633b721 100644 --- a/apps/web-ele/src/api/request.ts +++ b/apps/web-ele/src/api/request.ts @@ -16,6 +16,7 @@ import { useAccessStore } from '@vben/stores'; import { ElMessage } from 'element-plus'; import { useAuthStore } from '#/store'; +import { ApiEncrypt } from '#/utils/encrypt'; import { refreshTokenApi } from './core'; @@ -84,10 +85,46 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { config.headers['visit-tenant-id'] = tenantEnable ? accessStore.visitTenantId : undefined; + + // 是否 API 加密 + if ((config.headers || {}).isEncrypt) { + try { + // 加密请求数据 + if (config.data) { + config.data = ApiEncrypt.encryptRequest(config.data); + // 设置加密标识头 + config.headers[ApiEncrypt.getEncryptHeader()] = 'true'; + } + } catch (error) { + console.error('请求数据加密失败:', error); + throw error; + } + } return config; }, }); + // API 解密响应拦截器 + client.addResponseInterceptor({ + fulfilled: (response) => { + // 检查是否需要解密响应数据 + 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); + } catch (error) { + console.error('响应数据解密失败:', error); + throw new Error(`响应数据解密失败: ${(error as Error).message}`); + } + } + return response; + }, + }); + // 处理返回的响应数据格式 client.addResponseInterceptor( defaultResponseInterceptor({ diff --git a/apps/web-ele/src/utils/encrypt.ts b/apps/web-ele/src/utils/encrypt.ts new file mode 100644 index 000000000..1b478e864 --- /dev/null +++ b/apps/web-ele/src/utils/encrypt.ts @@ -0,0 +1,240 @@ +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/types/global.d.ts b/packages/types/global.d.ts index 5b3b3e91a..36e546ac2 100644 --- a/packages/types/global.d.ts +++ b/packages/types/global.d.ts @@ -11,6 +11,12 @@ export interface VbenAdminProAppConfigRaw { VITE_GLOB_API_URL: string; VITE_GLOB_AUTH_DINGDING_CLIENT_ID: string; VITE_GLOB_AUTH_DINGDING_CORP_ID: string; + // API 加解密相关配置 + VITE_APP_API_ENCRYPT_ENABLE: string; + VITE_APP_API_ENCRYPT_HEADER: string; + VITE_APP_API_ENCRYPT_ALGORITHM: string; + VITE_APP_API_ENCRYPT_REQUEST_KEY: string; + VITE_APP_API_ENCRYPT_RESPONSE_KEY: string; } interface AuthConfig { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5f4d89ff..735c64ac5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -330,6 +330,9 @@ catalogs: is-ci: specifier: ^4.1.0 version: 4.1.0 + jsencrypt: + specifier: ^3.3.2 + version: 3.5.4 json-bigint: specifier: ^1.0.0 version: 1.0.0 @@ -782,6 +785,9 @@ 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)) @@ -873,6 +879,9 @@ 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)) @@ -8074,6 +8083,9 @@ packages: resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} engines: {node: '>=12.0.0'} + jsencrypt@3.5.4: + resolution: {integrity: sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA==} + jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -18550,6 +18562,8 @@ snapshots: jsdoc-type-pratt-parser@4.1.0: {} + jsencrypt@3.5.4: {} + jsesc@3.0.2: {} jsesc@3.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 59d652674..1d22332ef 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -92,6 +92,7 @@ catalog: cropperjs: ^1.6.2 crypto-js: ^4.2.0 cspell: ^8.19.4 + jsencrypt: ^3.3.2 cssnano: ^7.0.7 cz-git: ^1.11.2 czg: ^1.11.1