feat:【framework 框架】增加 api 加解密能力

pull/813/head
YunaiV 2025-08-16 15:54:23 +08:00
parent efbc51659b
commit 2e796b8fc7
5 changed files with 284 additions and 1 deletions

9
.env
View File

@ -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==

View File

@ -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
}
})
}
// 注册

View File

@ -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 导出

231
src/utils/encrypt.ts Normal file
View File

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

6
types/env.d.ts vendored
View File

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