chad 16 часов назад
Родитель
Сommit
c43da4e27e
5 измененных файлов с 564 добавлено и 118 удалено
  1. 3 1
      .eslintrc-auto-import.json
  2. 21 1
      package-lock.json
  3. 2 0
      package.json
  4. 186 116
      src/request/index.js
  5. 352 0
      src/utils/apiCrypto.js

+ 3 - 1
.eslintrc-auto-import.json

@@ -128,6 +128,8 @@
     "watchPostEffect": true,
     "watchSyncEffect": true,
     "websocket": true,
-    "wsURL": true
+    "wsURL": true,
+    "decryptApiPayload": true,
+    "encryptApiPayload": true
   }
 }

+ 21 - 1
package-lock.json

@@ -27,18 +27,20 @@
         "@dcloudio/uni-mp-weixin": "3.0.0-4060620250520001",
         "@dcloudio/uni-mp-xhs": "3.0.0-4060620250520001",
         "@dcloudio/uni-quickapp-webview": "3.0.0-4060620250520001",
+        "@noble/ciphers": "^2.2.0",
         "@uni-helper/uni-env": "~0.1.7",
         "@uni-helper/uni-promises": "~0.2.1",
         "@uni-helper/vite-plugin-uni-components": "^0.2.10",
         "alova": "~3.2.11",
         "color": "~5.0.0",
+        "crypto-js": "^4.2.0",
         "es-toolkit": "~1.38.0",
         "pinia": "~2.0.36",
         "pinia-plugin-persistedstate": "~3.2.1",
         "uniapp-router-next": "~1.2.7",
         "uview-pro": "^0.5.7",
         "vconsole": "^3.15.1",
-        "vue": "3.4.21",
+        "vue": "^3.3.4",
         "vue-i18n": "~9.1.9",
         "wot-design-uni": "~1.9.1",
         "z-paging": "~2.8.6"
@@ -3624,6 +3626,18 @@
         "@jridgewell/sourcemap-codec": "^1.4.14"
       }
     },
+    "node_modules/@noble/ciphers": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmmirror.com/@noble/ciphers/-/ciphers-2.2.0.tgz",
+      "integrity": "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -5931,6 +5945,12 @@
         "node": ">= 8"
       }
     },
+    "node_modules/crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+      "license": "MIT"
+    },
     "node_modules/css-font-size-keywords": {
       "version": "1.0.0",
       "resolved": "https://registry.npmmirror.com/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz",

+ 2 - 0
package.json

@@ -66,11 +66,13 @@
     "@dcloudio/uni-mp-weixin": "3.0.0-4060620250520001",
     "@dcloudio/uni-mp-xhs": "3.0.0-4060620250520001",
     "@dcloudio/uni-quickapp-webview": "3.0.0-4060620250520001",
+    "@noble/ciphers": "^2.2.0",
     "@uni-helper/uni-env": "~0.1.7",
     "@uni-helper/uni-promises": "~0.2.1",
     "@uni-helper/vite-plugin-uni-components": "^0.2.10",
     "alova": "~3.2.11",
     "color": "~5.0.0",
+    "crypto-js": "^4.2.0",
     "es-toolkit": "~1.38.0",
     "pinia": "~2.0.36",
     "pinia-plugin-persistedstate": "~3.2.1",

+ 186 - 116
src/request/index.js

@@ -1,5 +1,11 @@
 import i18n, { defaultLang } from "@/lang/index.js";
 
+// 加解密
+import {
+    encryptApiPayload,
+    decryptApiPayload
+} from "@/utils/apiCrypto.js";
+
 // export const apiUrl = 'http://bot28api.sp2509.cc/api/'; // botapi的地址
 
 export var globalBaseURL = 'https://api.f1bet888.com/api'
@@ -14,6 +20,14 @@ export var apiUrl = 'https://bot-api.f1bet888.com/api/'; // botapi的地址
 // export var apiUrl = 'https://bot28api.sp2509.cc/api/'; // botapi的地址
 // #endif
 
+// 不需要加密的接口
+const encryptExcludeList = [
+    '/upload/',
+    '/egame/play'
+];
+function needEncrypt(url) {
+    return !encryptExcludeList.some(item => url.includes(item));
+}
 // 1. 封装 uni.request 请求
 const service = (config) => {
 
@@ -57,131 +71,187 @@ const service = (config) => {
         }
 
         // 4. 发起 uni.request 请求
-        uni.request({
-            // #ifdef H5
-            url: window.global?.baseURL + url, // 拼接基础地址和接口路径
-            // #endif
-
-            // #ifdef APP
-            url: globalBaseURL + url, // 拼接基础地址和接口路径
-            // url: 'https://sportapi.sp2509.cc/api' + url, // 拼接基础地址和接口路径
-            // #endif
-
-            // url: 'https://sportapi.sp2509.cc/api' + url, // 拼接基础地址和接口路径
-            // url: 'http://192.168.110.106:8084/api' + url, // 拼接基础地址和接口路径
-            method: method.toUpperCase(), // 确保请求方法为大写
-            data: data, // POST/PUT 等请求的参数
-            header: header,
-            timeout: timeout,
-            // 5. 请求成功回调
-            success: (response) => {
-                const res = response.data;
-
-                // 关闭 loading
-                if (loadingShowed) {
-                    uni.hideLoading();
-                }
+        const sendRequest = (requestData) => {
+            uni.request({
+                // #ifdef H5
+                url: window.global?.baseURL + url, // 拼接基础地址和接口路径
+                // #endif
+
+                // #ifdef APP
+                url: globalBaseURL + url, // 拼接基础地址和接口路径
+                // url: 'https://sportapi.sp2509.cc/api' + url, // 拼接基础地址和接口路径
+                // #endif
+
+                // url: 'https://sportapi.sp2509.cc/api' + url, // 拼接基础地址和接口路径
+                // url: 'http://192.168.110.106:8084/api' + url, // 拼接基础地址和接口路径
+                method: method.toUpperCase(), // 确保请求方法为大写
+                data: requestData, // POST/PUT 等请求的参数
+                header: header,
+                timeout: timeout,
+                // 5. 请求成功回调
+                success: (response) => {
+                    const handleResponse = (res) => {
+                        // 关闭 loading
+                        if (loadingShowed) {
+                            uni.hideLoading();
+                        }
+
+                        // 按 code 处理响应
+                        if (res.code === 1) {
+                            // 成功:按需显示成功提示
+                            if (showSuccessTip && res.msg) {
+                                uni.showToast({
+                                    title: res.msg,
+                                    icon: 'success',
+                                    duration: 2000,
+                                    position: 'middle'
+                                });
+                            }
+                            resolve(res); // 返回成功数据
+                        } else if (res.code === 0) {
+                            // 失败:按需显示失败提示
+                            if (showFailTip && res.msg) {
+                                uni.showToast({
+                                    title: res.msg,
+                                    icon: 'none',
+                                    duration: 2000,
+                                    position: 'middle'
+                                });
+                            }
+                            reject(new Error(res.msg || i18n.global.t('操作失败')));
+                        } else if (res.code === 401) {
+                            // 登录过期:强制显示提示
+                            uni.showToast({
+                                title: res.msg || i18n.global.t('登录已过期,请重新登录'),
+                                icon: 'none',
+                                duration: 2000,
+                                mask: true
+                            });
+                            // 清除 token + 跳登录页
+                            uni.removeStorageSync('token');
+                            uni.removeStorageSync('youke_token');
+                            let timer = setTimeout(() => {
+                                clearTimeout(timer);
+                                uni.navigateTo({
+                                    url: '/pages/login/index' // 注意路径要符合uni-app规范
+                                });
+                            }, 1500);
+                            reject(new Error('登录已过期'));
+                        } else if (res.code === 10011) {
+                            // 资金密码未设置
+                            // 加一个微小延迟防止被之前的loading遮挡
+                            let modalTimer = setTimeout(() => {
+                                clearTimeout(modalTimer);
+                                uni.showModal({
+                                    title: i18n.global.t('提示'),
+                                    content: i18n.global.t('您还未设置资金密码,是否前往设置?'),
+                                    success: (modalRes) => {
+                                        if (modalRes.confirm) {
+                                            uni.navigateTo({
+                                                url: '/pages/WorkModule/my/changeFundPassword/index'
+                                            });
+                                        }
+                                    }
+                                });
+                            }, 300);
+                            reject(new Error(res.msg || '未设置资金密码'));
+                        } else if (res.code === 10012) {
+                            // 触发全局事件,唤起自定义的高级通知弹窗
+                            // 如果后端接口返回了通知的文本内容,可以通过 res.data 传过去
+                            uni.$emit('show-system-notice-modal', res.data || res.msg);
+                            reject(new Error(res.msg || '系统重要通知'));
+                        } else {
+                            // 其他 code:按需显示失败提示
+                            if (showFailTip) {
+                                uni.showToast({
+                                    title: res.msg || i18n.global.t('请求失败,请稍后重试'),
+                                    icon: 'none',
+                                    duration: 2000
+                                });
+                            }
+                            reject(new Error(res.msg || '请求失败'));
+                        }
+                    };
+                    if (
+                        response.data &&
+                        response.data.encrypted === true &&
+                        response.data.payload
+                    ) {
+
+                        decryptApiPayload(
+                            response.data.payload,
+                            "response"
+                        )
+                            .then(result => {
+
+                                handleResponse(result);
+
+                            })
+                            .catch(err => {
+
+                                reject(err);
+
+                            });
+
+                    } else {
+
+                        handleResponse(response.data);
 
-                // 按 code 处理响应
-                if (res.code === 1) {
-                    // 成功:按需显示成功提示
-                    if (showSuccessTip && res.msg) {
-                        uni.showToast({
-                            title: res.msg,
-                            icon: 'success',
-                            duration: 2000,
-                            position: 'middle'
-                        });
                     }
-                    resolve(res); // 返回成功数据
-                } else if (res.code === 0) {
-                    // 失败:按需显示失败提示
-                    if (showFailTip && res.msg) {
-                        uni.showToast({
-                            title: res.msg,
-                            icon: 'none',
-                            duration: 2000,
-                            position: 'middle'
-                        });
+                },
+                // 6. 请求失败回调(网络错误、超时等)
+                fail: (error) => {
+                    console.log(error, 119);
+                    // 关闭 loading
+                    if (loadingShowed) {
+                        uni.hideLoading();
                     }
-                    reject(new Error(res.msg || i18n.global.t('操作失败')));
-                } else if (res.code === 401) {
-                    // 登录过期:强制显示提示
+                    // 显示网络错误提示
                     uni.showToast({
-                        title: res.msg || i18n.global.t('登录已过期,请重新登录'),
+                        // title: error.errMsg || i18n.global.t('网络异常,请检查网络后重试'),
+                        title: i18n.global.t('网络异常,请检查网络后重试'),
                         icon: 'none',
-                        duration: 2000,
-                        mask: true
+                        duration: 2000
                     });
-                    // 清除 token + 跳登录页
-                    uni.removeStorageSync('token');
-                    uni.removeStorageSync('youke_token');
-                    let timer = setTimeout(() => {
-                        clearTimeout(timer);
-                        uni.navigateTo({
-                            url: '/pages/login/index' // 注意路径要符合uni-app规范
-                        });
-                    }, 1500);
-                    reject(new Error('登录已过期'));
-                } else if (res.code === 10011) {
-                    // 资金密码未设置
-                    // 加一个微小延迟防止被之前的loading遮挡
-                    let modalTimer = setTimeout(() => {
-                        clearTimeout(modalTimer);
-                        uni.showModal({
-                            title: i18n.global.t('提示'),
-                            content: i18n.global.t('您还未设置资金密码,是否前往设置?'),
-                            success: (modalRes) => {
-                                if (modalRes.confirm) {
-                                    uni.navigateTo({
-                                        url: '/pages/WorkModule/my/changeFundPassword/index'
-                                    });
-                                }
-                            }
-                        });
-                    }, 300);
-                    reject(new Error(res.msg || '未设置资金密码'));
-                } else if (res.code === 10012) {
-                    // 触发全局事件,唤起自定义的高级通知弹窗
-                    // 如果后端接口返回了通知的文本内容,可以通过 res.data 传过去
-                    uni.$emit('show-system-notice-modal', res.data || res.msg);
-                    reject(new Error(res.msg || '系统重要通知'));
-                } else {
-                    // 其他 code:按需显示失败提示
-                    if (showFailTip) {
-                        uni.showToast({
-                            title: res.msg || i18n.global.t('请求失败,请稍后重试'),
-                            icon: 'none',
-                            duration: 2000
-                        });
+                    reject(error);
+                },
+                // 7. 请求完成(无论成功失败)
+                complete: () => {
+                    // 兜底关闭loading,防止遗漏
+                    if (loadingShowed) {
+                        uni.hideLoading();
                     }
-                    reject(new Error(res.msg || '请求失败'));
-                }
-            },
-            // 6. 请求失败回调(网络错误、超时等)
-            fail: (error) => {
-                console.log(error, 119);
-                // 关闭 loading
-                if (loadingShowed) {
-                    uni.hideLoading();
                 }
-                // 显示网络错误提示
-                uni.showToast({
-                    // title: error.errMsg || i18n.global.t('网络异常,请检查网络后重试'),
-                    title: i18n.global.t('网络异常,请检查网络后重试'),
-                    icon: 'none',
-                    duration: 2000
+            });
+        };
+
+        if (needEncrypt(url)) {
+
+            encryptApiPayload(data, "request")
+                .then(payload => {
+
+                    header["X-Api-Encrypted"] = "1";
+
+                    sendRequest({
+                        payload
+                    });
+
+                })
+                .catch(err => {
+
+                    if (loadingShowed) {
+                        uni.hideLoading();
+                    }
+
+                    reject(err);
+
                 });
-                reject(error);
-            },
-            // 7. 请求完成(无论成功失败)
-            complete: () => {
-                // 兜底关闭loading,防止遗漏
-                if (loadingShowed) {
-                    uni.hideLoading();
-                }
-            }
-        });
+
+        } else {
+
+            sendRequest(data);
+
+        }
     });
 };
 

+ 352 - 0
src/utils/apiCrypto.js

@@ -0,0 +1,352 @@
+console.log("window.crypto =", window.crypto);
+console.log("window.crypto?.subtle =", window.crypto?.subtle);
+console.log("globalThis.crypto =", globalThis.crypto);
+console.log("globalThis.crypto?.subtle =", globalThis.crypto?.subtle);
+const VERSION = 1;
+
+const IV_BYTES = 12;
+
+const TAG_BYTES = 16;
+
+
+const encoder = new TextEncoder();
+
+const decoder = new TextDecoder();
+
+
+// 你的后端生成的32字节base64密钥
+const API_KEY =
+    "3MRp/hTEz310NX3BJuMmIYAdlby1Q4K4XPeVSxXm5tE=";
+
+
+
+let cryptoKey = null;
+
+
+
+// base64url -> Uint8Array
+function base64UrlToBytes(value) {
+
+    const base64 =
+        value
+            .replace(/-/g, '+')
+            .replace(/_/g, '/');
+
+
+    const padded =
+        base64 +
+        '='.repeat(
+            (4 - base64.length % 4) % 4
+        );
+
+
+    const binary =
+        atob(padded);
+
+
+    return Uint8Array.from(
+        binary,
+        c => c.charCodeAt(0)
+    );
+
+}
+
+
+
+// Uint8Array -> base64url
+function bytesToBase64Url(bytes) {
+
+    let binary = '';
+
+
+    bytes.forEach(
+        b => {
+            binary +=
+                String.fromCharCode(b);
+        }
+    );
+
+
+    return btoa(binary)
+        .replace(/\+/g, '-')
+        .replace(/\//g, '_')
+        .replace(/=/g, '');
+
+}
+
+
+
+// 获取AES KEY
+async function getKey() {
+
+
+    if (cryptoKey) {
+        return cryptoKey;
+    }
+
+
+
+    cryptoKey =
+        await crypto.subtle.importKey(
+
+            "raw",
+
+            base64UrlToBytes(
+                API_KEY
+            ),
+
+            {
+                name: "AES-GCM"
+            },
+
+            false,
+
+            [
+                "encrypt",
+                "decrypt"
+            ]
+
+        );
+
+
+    return cryptoKey;
+
+}
+
+
+
+// AAD
+function getAAD(
+    direction,
+    ts
+) {
+
+    return encoder.encode(
+
+        `api-content:v1:${direction}:${ts}`
+
+    );
+
+}
+
+
+
+// 加密请求
+export async function encryptApiPayload(
+    data,
+    direction = "request"
+) {
+
+
+    const ts =
+        Date.now();
+
+
+
+    const iv =
+        crypto.getRandomValues(
+            new Uint8Array(
+                IV_BYTES
+            )
+        );
+
+
+
+    const plaintext =
+        encoder.encode(
+            JSON.stringify(data)
+        );
+
+
+
+    const encrypted =
+        new Uint8Array(
+
+            await crypto.subtle.encrypt(
+
+                {
+
+                    name: "AES-GCM",
+
+                    iv,
+
+                    additionalData:
+                        getAAD(
+                            direction,
+                            ts
+                        ),
+
+                    tagLength:
+                        TAG_BYTES * 8
+
+                },
+
+
+                await getKey(),
+
+
+                plaintext
+
+            )
+
+        );
+
+
+
+    // WebCrypto返回 ciphertext+tag
+    const tag =
+        encrypted.slice(
+            -TAG_BYTES
+        );
+
+
+    const cipherText =
+        encrypted.slice(
+            0,
+            -TAG_BYTES
+        );
+
+
+
+    const envelope = {
+
+        v: VERSION,
+
+        ts,
+
+        iv:
+            bytesToBase64Url(iv),
+
+
+        data:
+            bytesToBase64Url(
+                cipherText
+            ),
+
+
+        tag:
+            bytesToBase64Url(
+                tag
+            )
+
+    };
+
+
+
+    return bytesToBase64Url(
+
+        encoder.encode(
+
+            JSON.stringify(
+                envelope
+            )
+
+        )
+
+    );
+
+}
+
+
+
+
+// 解密响应
+export async function decryptApiPayload(
+    payload,
+    direction = "response"
+) {
+
+
+    const envelope =
+        JSON.parse(
+
+            decoder.decode(
+
+                base64UrlToBytes(
+                    payload
+                )
+
+            )
+
+        );
+
+
+
+    const iv =
+        base64UrlToBytes(
+            envelope.iv
+        );
+
+
+    const data =
+        base64UrlToBytes(
+            envelope.data
+        );
+
+
+    const tag =
+        base64UrlToBytes(
+            envelope.tag
+        );
+
+
+
+    const encrypted =
+        new Uint8Array(
+
+            data.length +
+            tag.length
+
+        );
+
+
+
+    encrypted.set(data);
+
+
+    encrypted.set(
+        tag,
+        data.length
+    );
+
+
+
+    const plaintext =
+        await crypto.subtle.decrypt(
+
+            {
+
+                name: "AES-GCM",
+
+                iv,
+
+                additionalData:
+                    getAAD(
+                        direction,
+                        envelope.ts
+                    ),
+
+                tagLength:
+                    TAG_BYTES * 8
+
+            },
+
+
+            await getKey(),
+
+
+            encrypted
+
+        );
+
+
+
+    return JSON.parse(
+
+        decoder.decode(
+            plaintext
+        )
+
+    );
+
+}