From 2e796b8fc730d17d11bb780979917ba33c03a5c1 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 16 Aug 2025 15:54:23 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E3=80=90framework=20=E6=A1=86?= =?UTF-8?q?=E6=9E=B6=E3=80=91=E5=A2=9E=E5=8A=A0=20api=20=E5=8A=A0=E8=A7=A3?= =?UTF-8?q?=E5=AF=86=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 9 ++ src/api/login/index.ts | 8 +- src/config/axios/service.ts | 31 +++++ src/utils/encrypt.ts | 231 ++++++++++++++++++++++++++++++++++++ types/env.d.ts | 6 + 5 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 src/utils/encrypt.ts diff --git a/.env b/.env index 4b0f5bf6a..791102202 100644 --- a/.env +++ b/.env @@ -23,3 +23,12 @@ VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc VITE_APP_DEFAULT_LOGIN_TENANT = 芋道源码 VITE_APP_DEFAULT_LOGIN_USERNAME = admin VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123 + +# 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== \ No newline at end of file diff --git a/src/api/login/index.ts b/src/api/login/index.ts index 7d7d407b4..cd5986a0f 100644 --- a/src/api/login/index.ts +++ b/src/api/login/index.ts @@ -13,7 +13,13 @@ export interface SmsLoginVO { // 登录 export const login = (data: UserLoginVO) => { - return request.post({ url: '/system/auth/login', data }) + return request.post({ + url: '/system/auth/login', + data, + headers: { + isEncrypt: true + } + }) } // 注册 diff --git a/src/config/axios/service.ts b/src/config/axios/service.ts index 74280a925..9214bf8ea 100644 --- a/src/config/axios/service.ts +++ b/src/config/axios/service.ts @@ -15,6 +15,7 @@ import errorCode from './errorCode' import { resetRouter } from '@/router' import { deleteUserCache } from '@/hooks/web/useCache' +import { ApiEncrypt } from '@/utils/encrypt' const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE const { result_code, base_url, request_timeout } = config @@ -83,6 +84,20 @@ service.interceptors.request.use( } } } + // 是否 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 }, (error: AxiosError) => { @@ -101,6 +116,22 @@ service.interceptors.response.use( // 返回“[HTTP]请求没有返回值”; throw new Error() } + + // 检查是否需要解密响应数据 + const encryptHeader = ApiEncrypt.getEncryptHeader() + const isEncryptResponse = + response.headers[encryptHeader] === 'true' || + response.headers[encryptHeader.toLowerCase()] === 'true' + if (isEncryptResponse && typeof data === 'string') { + try { + // 解密响应数据 + data = ApiEncrypt.decryptResponse(data) + } catch (error) { + console.error('响应数据解密失败:', error) + throw new Error('响应数据解密失败: ' + (error as Error).message) + } + } + const { t } = useI18n() // 未设置状态码则默认成功状态 // 二进制数据则直接返回,例如说 Excel 导出 diff --git a/src/utils/encrypt.ts b/src/utils/encrypt.ts new file mode 100644 index 000000000..aee289e30 --- /dev/null +++ b/src/utils/encrypt.ts @@ -0,0 +1,231 @@ +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 class AES { + /** + * AES 加密 + * @param data 要加密的数据 + * @param key 加密密钥 + * @returns 加密后的字符串 + */ + static 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 解密后的字符串 + */ + static 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 class RSA { + /** + * RSA 加密 + * @param data 要加密的数据 + * @param publicKey 公钥(必需) + * @returns 加密后的字符串 + */ + static encrypt(data: string, publicKey: string): string | false { + 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 解密后的字符串 + */ + static decrypt(encryptedData: string, privateKey: string): string | false { + 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 class ApiEncrypt { + /** + * 获取加密头名称 + */ + static getEncryptHeader(): string { + return API_ENCRYPT_HEADER + } + + /** + * 加密请求数据 + * @param data 要加密的数据 + * @returns 加密后的数据 + */ + static 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 解密后的数据 + */ + static decryptResponse(encryptedData: string): any { + if (!API_ENCRYPT_ENABLE) { + return encryptedData + } + + try { + let decryptedData: string | false = '' + 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/types/env.d.ts b/types/env.d.ts index 124dd5656..17535eafc 100644 --- a/types/env.d.ts +++ b/types/env.d.ts @@ -26,6 +26,12 @@ interface ImportMetaEnv { readonly VITE_SOURCEMAP: string readonly VITE_OUT_DIR: string readonly VITE_GOVIEW_URL: string + // API 加解密相关配置 + readonly VITE_APP_API_ENCRYPT_ENABLE: string + readonly VITE_APP_API_ENCRYPT_HEADER: string + readonly VITE_APP_API_ENCRYPT_ALGORITHM: string + readonly VITE_APP_API_ENCRYPT_REQUEST_KEY: string + readonly VITE_APP_API_ENCRYPT_RESPONSE_KEY: string } declare global {