Bläddra i källkod

!36 feat: 新增API加解密,通过options设置isEncrypt
Merge pull request !36 from 熊猫大侠/master-dev-apienc

芋道源码 4 månader sedan
förälder
incheckning
205d5f977d
6 ändrade filer med 279 tillägg och 6 borttagningar
  1. 8 1
      env/.env
  2. 1 0
      package.json
  3. 15 2
      src/http/http.ts
  4. 22 3
      src/http/interceptor.ts
  5. 2 0
      src/http/types.ts
  6. 231 0
      src/utils/encrypt.ts

+ 8 - 1
env/.env

@@ -40,4 +40,11 @@ VITE_APP_CAPTCHA_ENABLE=false
 # 默认账户密码
 VITE_APP_DEFAULT_LOGIN_TENANT_ID = 1
 VITE_APP_DEFAULT_LOGIN_USERNAME = admin
-VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123
+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

+ 1 - 0
package.json

@@ -115,6 +115,7 @@
     "abortcontroller-polyfill": "^1.7.8",
     "crypto-js": "^4.2.0",
     "dayjs": "1.11.10",
+    "jsencrypt": "^3.5.4",
     "pinia": "2.0.36",
     "pinia-plugin-persistedstate": "3.2.1",
     "vue": "^3.4.21",

+ 15 - 2
src/http/http.ts

@@ -3,6 +3,7 @@ import type { CustomRequestOptions, IResponse } from '@/http/types'
 import { nextTick } from 'vue'
 import { useTokenStore } from '@/store/token'
 import { getLastPage, isDoubleTokenMode } from '@/utils'
+import { ApiEncrypt } from '@/utils/encrypt'
 import { toLoginPage } from '@/utils/toLoginPage'
 import { ResultEnum } from './tools/enum'
 
@@ -21,9 +22,21 @@ export function http<T>(options: CustomRequestOptions) {
       // #endif
       // 响应成功
       success: async (res) => {
-        const responseData = res.data as IResponse<T>
-        const { code } = responseData
+        let responseData = res.data as IResponse<T>
+        // 检查是否需要解密响应数据
+        const encryptHeader = ApiEncrypt.getEncryptHeader()
+        const isEncryptResponse = res.header[encryptHeader] === 'true' || res.header[encryptHeader.toLowerCase()] === 'true'
+        if (isEncryptResponse && typeof responseData === 'string') {
+          try {
+            // 解密响应数据
+            responseData = ApiEncrypt.decryptResponse(responseData)
+          } catch (error) {
+            console.error('响应数据解密失败:', error)
+            throw new Error(`响应数据解密失败: ${(error as Error).message}`)
+          }
+        }
 
+        const { code } = responseData
         // 检查是否是401错误(包括HTTP状态码401或业务码401)
         const isTokenExpired = res.statusCode === 401 || code === 401
 

+ 22 - 3
src/http/interceptor.ts

@@ -2,6 +2,7 @@
 import type { CustomRequestOptions } from '@/http/types'
 import { useTokenStore, useUserStore } from '@/store'
 import { getEnvBaseUrl } from '@/utils'
+import { ApiEncrypt } from '@/utils/encrypt'
 import { stringifyQuery } from './tools/queryString'
 
 // 请求基准地址
@@ -55,11 +56,13 @@ const httpInterceptor = {
     const tokenStore = useTokenStore()
     const token = tokenStore.validToken
     let isToken = (options!.header || {}).isToken === false
-    whiteList.some((v) => {
+
+    for (const v of whiteList) {
       if (options.url && options.url.includes(v)) {
-        return (isToken = false)
+        isToken = false
+        break
       }
-    })
+    }
     if (!isToken && token) {
       options.header.Authorization = `Bearer ${token}`
     }
@@ -71,6 +74,22 @@ const httpInterceptor = {
         options.header['tenant-id'] = tenantId
       }
     }
+
+    // 5. 是否 API 加密
+    if (options.isEncrypt) {
+      try {
+        // 加密请求数据
+        if (options.data) {
+          options.data = ApiEncrypt.encryptRequest(options.data)
+          // 设置加密标识头
+          options.header[ApiEncrypt.getEncryptHeader()] = 'true'
+        }
+      } catch (error) {
+        console.error('请求数据加密失败:', error)
+        throw error
+      }
+    }
+
     return options
   },
 }

+ 2 - 0
src/http/types.ts

@@ -7,6 +7,8 @@ export type CustomRequestOptions = UniApp.RequestOptions & {
   hideErrorToast?: boolean
   /** 是否返回原始数据 add by panda 25.12.10 */
   original?: boolean
+  /** 是否API加密 add by panda 25.12.24 */
+  isEncrypt?: boolean
 } & IUniUploadFileOptions // 添加uni.uploadFile参数类型
 
 // 通用响应格式(兼容 msg + message 字段)

+ 231 - 0
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
+    }
+  }
+}