feat:添加接口签名
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ lerna-debug.log*
|
|||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
|
xSynergy-manage
|
||||||
dist-ssr
|
dist-ssr
|
||||||
coverage
|
coverage
|
||||||
*.local
|
*.local
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -27,3 +27,24 @@ npm run dev
|
|||||||
```sh
|
```sh
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1.在本地启动项目时,要修改签名的host,在utils/tools.js
|
||||||
|
|
||||||
|
```
|
||||||
|
// 获取host(从baseURL或当前域名) 本地启动要求host为xsynergy.gxtech.ltd
|
||||||
|
// let host = 'xsynergy.gxtech.ltd';
|
||||||
|
let host = '';
|
||||||
|
if (config.baseURL) {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(config.baseURL);
|
||||||
|
host = urlObj.host;
|
||||||
|
} catch (e) {
|
||||||
|
host = window.location.host;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
host = window.location.host;
|
||||||
|
}
|
||||||
|
```
|
||||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -15,9 +15,11 @@
|
|||||||
"@vueuse/core": "^9.5.0",
|
"@vueuse/core": "^9.5.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
"blakejs": "^1.2.1",
|
||||||
"code-inspector-plugin": "^0.20.12",
|
"code-inspector-plugin": "^0.20.12",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"element-plus": "^2.2.27",
|
"element-plus": "^2.2.27",
|
||||||
|
"hashids": "^1.2.2",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"livekit-client": "^2.7.5",
|
"livekit-client": "^2.7.5",
|
||||||
"mitt": "^3.0.0",
|
"mitt": "^3.0.0",
|
||||||
@@ -2144,6 +2146,11 @@
|
|||||||
"readable-stream": "^4.2.0"
|
"readable-stream": "^4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/blakejs": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ=="
|
||||||
|
},
|
||||||
"node_modules/bluebird": {
|
"node_modules/bluebird": {
|
||||||
"version": "3.7.2",
|
"version": "3.7.2",
|
||||||
"resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz",
|
"resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz",
|
||||||
@@ -4563,6 +4570,11 @@
|
|||||||
"minimalistic-assert": "^1.0.1"
|
"minimalistic-assert": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hashids": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hashids/-/hashids-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-dEHCG2LraR6PNvSGxosZHIRgxF5sNLOIBFEHbj8lfP9WWmu/PWPMzsip1drdVSOFi51N2pU7gZavrgn7sbGFuw=="
|
||||||
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
|||||||
@@ -15,9 +15,11 @@
|
|||||||
"@vueuse/core": "^9.5.0",
|
"@vueuse/core": "^9.5.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
"blakejs": "^1.2.1",
|
||||||
"code-inspector-plugin": "^0.20.12",
|
"code-inspector-plugin": "^0.20.12",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"element-plus": "^2.2.27",
|
"element-plus": "^2.2.27",
|
||||||
|
"hashids": "^1.2.2",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"livekit-client": "^2.7.5",
|
"livekit-client": "^2.7.5",
|
||||||
"mitt": "^3.0.0",
|
"mitt": "^3.0.0",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
// import request from '@/views/custom/Meter/public/request.js'
|
|
||||||
|
|
||||||
// 登录方法
|
// 登录方法
|
||||||
export function login(username, password) {
|
export function login(username, password) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ElMessage,
|
ElMessage,
|
||||||
} from "element-plus";
|
} from "element-plus";
|
||||||
import { tansParams } from "@/utils/ruoyi";
|
import { tansParams } from "@/utils/ruoyi";
|
||||||
|
import { generateSign } from "@/utils/tools";
|
||||||
import cache from "@/plugins/cache";
|
import cache from "@/plugins/cache";
|
||||||
import { getToken, removeToken } from "@/utils/auth";
|
import { getToken, removeToken } from "@/utils/auth";
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
@@ -38,7 +39,7 @@ const service = axios.create({
|
|||||||
|
|
||||||
// request拦截器
|
// request拦截器
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
(config) => {
|
async (config) => {
|
||||||
// 在拦截器内部安全地使用 store
|
// 在拦截器内部安全地使用 store
|
||||||
let sudid = ''
|
let sudid = ''
|
||||||
try {
|
try {
|
||||||
@@ -66,7 +67,6 @@ service.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
// get请求映射params参数
|
// get请求映射params参数
|
||||||
if (config.method === "get" && config.params) {
|
if (config.method === "get" && config.params) {
|
||||||
|
|
||||||
let url = config.url + "?" + tansParams(config.params);
|
let url = config.url + "?" + tansParams(config.params);
|
||||||
url = url.slice(0, -1);
|
url = url.slice(0, -1);
|
||||||
config.params = {};
|
config.params = {};
|
||||||
@@ -118,6 +118,22 @@ service.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log(sudid,'sudid')
|
||||||
|
// 生成sign并添加到URL
|
||||||
|
if (sudid) {
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
const sign = await generateSign(config, sudid, now);
|
||||||
|
if (sign) {
|
||||||
|
// 将sign添加到URL
|
||||||
|
const separator = config.url.includes('?') ? '&' : '?';
|
||||||
|
config.url += `${separator}sign=${encodeURIComponent(sign)}`;
|
||||||
|
}
|
||||||
|
} catch (signError) {
|
||||||
|
console.error('添加sign失败:', signError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@@ -125,8 +141,28 @@ service.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let isShowingError = false;
|
||||||
|
const showError = (message) => {
|
||||||
|
if (isShowingError) return;
|
||||||
|
isShowingError = true;
|
||||||
|
|
||||||
|
ElMessage({
|
||||||
|
message,
|
||||||
|
type: 'error',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isShowingError = false;
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
service.interceptors.response.use(
|
service.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
|
try {
|
||||||
|
// console.log('原始响应:', response);
|
||||||
// 1. 检查响应是否存在
|
// 1. 检查响应是否存在
|
||||||
if (!response) {
|
if (!response) {
|
||||||
ElMessage.error('无响应数据');
|
ElMessage.error('无响应数据');
|
||||||
@@ -145,6 +181,8 @@ service.interceptors.response.use(
|
|||||||
return responseData;
|
return responseData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log('响应数据:', responseData);
|
||||||
|
|
||||||
// 4. 根据业务码处理不同情况
|
// 4. 根据业务码处理不同情况
|
||||||
switch (businessCode) {
|
switch (businessCode) {
|
||||||
case 200:
|
case 200:
|
||||||
@@ -162,35 +200,97 @@ service.interceptors.response.use(
|
|||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
case 500:
|
case 500:
|
||||||
const serverErrorMsg = responseData.meta?.message || '服务器内部错误';
|
const serverErrorMsg = responseData.meta?.message || responseData.meta?.msg || '服务器内部错误';
|
||||||
ElMessage({ message: serverErrorMsg, type: 'error' });
|
ElMessage({ message: serverErrorMsg, type: 'error' });
|
||||||
return Promise.reject({ code: 500, message: serverErrorMsg });
|
return Promise.reject({ code: 500, message: serverErrorMsg });
|
||||||
|
|
||||||
default:
|
default:
|
||||||
const errorMsg = responseData.meta?.message || `业务错误 (${businessCode})`;
|
const errorMsg = responseData.meta?.message || responseData.meta?.msg || `业务错误 (${businessCode})`;
|
||||||
ElNotification.error({ title: errorMsg });
|
ElNotification.error({ title: errorMsg });
|
||||||
return Promise.reject({ code: businessCode, message: errorMsg });
|
return Promise.reject({ code: businessCode, message: errorMsg });
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('响应拦截器异常:', err);
|
||||||
|
|
||||||
|
showError('响应处理异常');
|
||||||
|
|
||||||
|
return Promise.reject({
|
||||||
|
code: -1,
|
||||||
|
message: '响应处理异常',
|
||||||
|
raw: err
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
// (error) => {
|
||||||
|
// let { message } = error;
|
||||||
|
// let code = error?.response?.status || -1;
|
||||||
|
// if (message == 'Network Error') {
|
||||||
|
// message = '后端接口连接异常';
|
||||||
|
// ElMessage({ message, type: 'error', duration: 5 * 1000 });
|
||||||
|
// } else if (message.includes('timeout')) {
|
||||||
|
// message = '系统接口请求超时';
|
||||||
|
// ElMessage({ message, type: 'error', duration: 5 * 1000 });
|
||||||
|
// } else if (message.includes('Request failed with status code')) {
|
||||||
|
// // message = '系统接口' + message.substr(message.length - 3) + '异常';
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 返回结构化错误
|
||||||
|
// return Promise.reject({
|
||||||
|
// code,
|
||||||
|
// message,
|
||||||
|
// raw: error // 保留原始 error
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
(error) => {
|
(error) => {
|
||||||
let { message } = error;
|
try {
|
||||||
|
let message = error?.message || '请求失败';
|
||||||
let code = error?.response?.status || -1;
|
let code = error?.response?.status || -1;
|
||||||
if (message == 'Network Error') {
|
if (message === 'Network Error') {
|
||||||
message = '后端接口连接异常';
|
message = '后端接口连接异常';
|
||||||
ElMessage({ message, type: 'error', duration: 5 * 1000 });
|
|
||||||
} else if (message.includes('timeout')) {
|
} else if (message.includes('timeout')) {
|
||||||
message = '系统接口请求超时';
|
message = '系统接口请求超时';
|
||||||
ElMessage({ message, type: 'error', duration: 5 * 1000 });
|
|
||||||
} else if (message.includes('Request failed with status code')) {
|
|
||||||
// message = '系统接口' + message.substr(message.length - 3) + '异常';
|
|
||||||
}
|
}
|
||||||
|
else if (code) {
|
||||||
|
const statusMap = {
|
||||||
|
400: '请求参数错误',
|
||||||
|
401: '登录已过期,请重新登录',
|
||||||
|
403: '没有权限访问',
|
||||||
|
404: '请求资源不存在',
|
||||||
|
408: '请求超时,请稍后重试',
|
||||||
|
409: '数据冲突,请刷新后重试',
|
||||||
|
422: '参数校验失败',
|
||||||
|
|
||||||
|
500: '服务器异常,请联系管理员',
|
||||||
|
501: '功能暂未实现',
|
||||||
|
502: '服务器内部错误,请联系管理员',
|
||||||
|
503: '服务繁忙,请稍后重试',
|
||||||
|
504: '服务器响应超时,请稍后重试'
|
||||||
|
};
|
||||||
|
|
||||||
|
message = message || statusMap[code];
|
||||||
|
|
||||||
|
// 401 特殊处理(不影响你原逻辑)
|
||||||
|
if (code === 401) {
|
||||||
|
return handleUnauthorized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showError(message);
|
||||||
|
|
||||||
// 返回结构化错误
|
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
code,
|
code,
|
||||||
message,
|
message,
|
||||||
raw: error // 保留原始 error
|
raw: error
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('错误拦截器异常:', err);
|
||||||
|
|
||||||
|
return Promise.reject({
|
||||||
|
code: -1,
|
||||||
|
message: '网络异常处理失败',
|
||||||
|
raw: err
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import Hashids from 'hashids';
|
||||||
|
import blake from 'blakejs';
|
||||||
|
import CryptoJS from 'crypto-js';
|
||||||
|
|
||||||
// 获取uuid
|
// 获取uuid
|
||||||
export function generateUUID() {
|
export function generateUUID() {
|
||||||
var d = new Date().getTime();
|
var d = new Date().getTime();
|
||||||
@@ -158,3 +162,429 @@ function isValidBox(x1, y1, x2, y2, imgWidth, imgHeight) {
|
|||||||
x2 <= imgWidth && y2 <= imgHeight
|
x2 <= imgWidth && y2 <= imgHeight
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 全局临时变量,用于存储最近一次请求的签名信息
|
||||||
|
let lastRequestKey = '';
|
||||||
|
let lastTimestamp = 0;
|
||||||
|
let lastUsedTimestamp = 0;
|
||||||
|
let collisionCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成请求唯一标识key
|
||||||
|
* @param {number} timestamp - 时间戳
|
||||||
|
* @param {string} method - 请求方法
|
||||||
|
* @param {string} urlPath - API路径
|
||||||
|
* @returns {string} 唯一标识
|
||||||
|
*/
|
||||||
|
function getRequestKey(timestamp, method, urlPath) {
|
||||||
|
return `${timestamp}_${method}_${urlPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取去重后的时间戳
|
||||||
|
* 处理同一毫秒内相同接口的重复请求
|
||||||
|
* @param {number} originalTimestamp - 原始时间戳
|
||||||
|
* @param {string} method - 请求方法
|
||||||
|
* @param {string} urlPath - API路径
|
||||||
|
* @returns {number} 处理后的时间戳
|
||||||
|
*/
|
||||||
|
function getDeduplicatedTimestamp(originalTimestamp, method, urlPath) {
|
||||||
|
const currentKey = getRequestKey(originalTimestamp, method, urlPath);
|
||||||
|
|
||||||
|
// 如果当前请求与上一次请求的时间戳、方法、API路径完全相同
|
||||||
|
if (currentKey === lastRequestKey && originalTimestamp === lastTimestamp) {
|
||||||
|
// 碰撞次数增加
|
||||||
|
collisionCount++;
|
||||||
|
// 时间戳增加碰撞次数毫秒
|
||||||
|
const adjustedTimestamp = originalTimestamp + collisionCount;
|
||||||
|
console.warn(`检测到重复请求: ${method} ${urlPath} @ ${originalTimestamp}ms, 调整时间戳为: ${adjustedTimestamp}ms (碰撞次数: ${collisionCount})`);
|
||||||
|
return adjustedTimestamp;
|
||||||
|
} else {
|
||||||
|
// 不同请求,重置碰撞计数
|
||||||
|
collisionCount = 0;
|
||||||
|
// 更新最后一次请求的记录
|
||||||
|
lastRequestKey = currentKey;
|
||||||
|
lastTimestamp = originalTimestamp;
|
||||||
|
return originalTimestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成sign签名
|
||||||
|
* @param {Object} config - axios请求配置
|
||||||
|
* @param {string} udid - 用户设备ID(22位短udid)
|
||||||
|
* @param {number} timestamp - 时间戳(毫秒) - 由调用方传入,确保与生成salt时使用的时间一致,避免因时间差导致的sign不匹配问题·
|
||||||
|
* @returns {Promise<string>} sign值
|
||||||
|
*/
|
||||||
|
export async function generateSign(config, udid, timestamp) {
|
||||||
|
try {
|
||||||
|
// 1. 准备模版字典
|
||||||
|
const reqMap = new Map();
|
||||||
|
|
||||||
|
// 获取请求方法
|
||||||
|
const method = config.method?.toUpperCase() || 'GET';
|
||||||
|
|
||||||
|
// 获取host(从baseURL或当前域名) 本地启动要求host为xsynergy.gxtech.ltd
|
||||||
|
let host = 'xsynergy.gxtech.ltd';
|
||||||
|
// let host = '';
|
||||||
|
// // console.log('请求配置:', config.baseURL)
|
||||||
|
// if (config.baseURL) {
|
||||||
|
// try {
|
||||||
|
// const urlObj = new URL(config.baseURL);
|
||||||
|
// // console.log('解析baseURL成功:', urlObj);
|
||||||
|
// host = urlObj.host;
|
||||||
|
// } catch (e) {
|
||||||
|
// // console.log('解析baseURL失败,使用window.location.host:', window.location);
|
||||||
|
// host = window.location.host;
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// host = window.location.host;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// console.log('请求host:', host);
|
||||||
|
|
||||||
|
// 获取完整URL路径(不含域名和查询参数)
|
||||||
|
let urlPath = config.url || '';
|
||||||
|
// 移除baseURL部分
|
||||||
|
if (config.baseURL && urlPath.startsWith(config.baseURL)) {
|
||||||
|
urlPath = urlPath.substring(config.baseURL.length);
|
||||||
|
}
|
||||||
|
// 移除查询参数
|
||||||
|
const queryIndex = urlPath.indexOf('?');
|
||||||
|
if (queryIndex !== -1) {
|
||||||
|
urlPath = urlPath.substring(0, queryIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取user-agent和token
|
||||||
|
const userAgent = config.headers?.['X-User-Agent'];
|
||||||
|
// const userAgent = config.headers?.['X-User-Agent'] || navigator.userAgent;
|
||||||
|
const token = config.headers?.['Authorization'] || '';
|
||||||
|
|
||||||
|
// 填充基础模板 - agent字段包含userAgent和token
|
||||||
|
reqMap.set('host', [host]);
|
||||||
|
reqMap.set('method', [method]);
|
||||||
|
reqMap.set('agent', [userAgent, token]);
|
||||||
|
reqMap.set('uri', [urlPath]);
|
||||||
|
|
||||||
|
// 2. 处理URL查询参数(不包括sign本身)
|
||||||
|
let fullUrl = config.url || '';
|
||||||
|
if (config.baseURL && !fullUrl.startsWith('http')) {
|
||||||
|
fullUrl = config.baseURL + fullUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(fullUrl, window.location.origin);
|
||||||
|
const searchParams = urlObj.searchParams;
|
||||||
|
|
||||||
|
// 遍历所有查询参数,排除sign
|
||||||
|
for (const [key, value] of searchParams.entries()) {
|
||||||
|
if (key !== 'sign') {
|
||||||
|
// 处理数组参数(key[]格式)
|
||||||
|
const normalizedKey = key.replace('[]', '');
|
||||||
|
if (reqMap.has(normalizedKey)) {
|
||||||
|
reqMap.get(normalizedKey).push(value);
|
||||||
|
} else {
|
||||||
|
reqMap.set(normalizedKey, [value]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('解析URL参数失败:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 处理GET请求的params参数
|
||||||
|
if (config.method?.toLowerCase() === 'get' && config.params) {
|
||||||
|
for (const [key, value] of Object.entries(config.params)) {
|
||||||
|
if (key !== 'sign') {
|
||||||
|
const strValue = String(value);
|
||||||
|
const normalizedKey = key.replace('[]', '');
|
||||||
|
if (reqMap.has(normalizedKey)) {
|
||||||
|
reqMap.get(normalizedKey).push(strValue);
|
||||||
|
} else {
|
||||||
|
reqMap.set(normalizedKey, [strValue]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 处理请求体(POST、PUT等)
|
||||||
|
if (config.method?.toLowerCase() !== 'get' && config.data) {
|
||||||
|
const contentType = config.headers?.['Content-Type'] || '';
|
||||||
|
|
||||||
|
if (contentType.includes('application/json')) {
|
||||||
|
// JSON数据 - 后端用"json"作为key
|
||||||
|
const jsonStr = typeof config.data === 'object'
|
||||||
|
? JSON.stringify(config.data)
|
||||||
|
: String(config.data);
|
||||||
|
reqMap.set('json', [jsonStr]);
|
||||||
|
|
||||||
|
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||||
|
// form-urlencoded数据
|
||||||
|
let formData = {};
|
||||||
|
if (typeof config.data === 'object') {
|
||||||
|
formData = config.data;
|
||||||
|
} else if (typeof config.data === 'string') {
|
||||||
|
const params = new URLSearchParams(config.data);
|
||||||
|
formData = Object.fromEntries(params.entries());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加每个表单字段到reqMap
|
||||||
|
for (const [key, value] of Object.entries(formData)) {
|
||||||
|
const strValue = String(value);
|
||||||
|
const normalizedKey = key.replace('[]', '');
|
||||||
|
if (reqMap.has(normalizedKey)) {
|
||||||
|
reqMap.get(normalizedKey).push(strValue);
|
||||||
|
} else {
|
||||||
|
reqMap.set(normalizedKey, [strValue]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (contentType.includes('multipart/form-data')) {
|
||||||
|
// multipart/form-data
|
||||||
|
if (config.data instanceof FormData) {
|
||||||
|
for (const [key, value] of config.data.entries()) {
|
||||||
|
if (!(value instanceof File || value instanceof Blob)) {
|
||||||
|
const strValue = String(value);
|
||||||
|
const normalizedKey = key.replace('[]', '');
|
||||||
|
if (reqMap.has(normalizedKey)) {
|
||||||
|
reqMap.get(normalizedKey).push(strValue);
|
||||||
|
} else {
|
||||||
|
reqMap.set(normalizedKey, [strValue]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 生成salt - 完全匹配后端的genSalt函数
|
||||||
|
// 后端: GenTokenSalt(uid, udid + signSalt)
|
||||||
|
// signSalt固定为: publicSignSalt
|
||||||
|
const signSalt = 'publicSignSalt';
|
||||||
|
const salt = await generateSalt(udid, signSalt);
|
||||||
|
|
||||||
|
// 6. 参数排序并进行HMAC-MD5哈希
|
||||||
|
const sortedKeys = Array.from(reqMap.keys()).sort();
|
||||||
|
// console.log('排序后的参数键:', sortedKeys);
|
||||||
|
|
||||||
|
// 创建HMAC-MD5
|
||||||
|
const hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.MD5, salt);
|
||||||
|
|
||||||
|
// 按排序后的key拼接值
|
||||||
|
let contentStr = '';
|
||||||
|
for (const key of sortedKeys) {
|
||||||
|
const values = reqMap.get(key);
|
||||||
|
const joinedValue = values.join('');
|
||||||
|
contentStr += joinedValue;
|
||||||
|
hmac.update(joinedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最终的hash字符串
|
||||||
|
const hashStr = hmac.finalize().toString(CryptoJS.enc.Hex).toUpperCase();
|
||||||
|
|
||||||
|
// 7. 分段拆散计算
|
||||||
|
const ints = new Array(6).fill(0);
|
||||||
|
|
||||||
|
// 拆封模版
|
||||||
|
const parts = [
|
||||||
|
[0, 7],
|
||||||
|
[7, 7],
|
||||||
|
[14, 4],
|
||||||
|
[18, 7],
|
||||||
|
[25, 7]
|
||||||
|
];
|
||||||
|
|
||||||
|
// 使用模版将hashStr拆为5个hex字串并转为整数,再倒序排列
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
// console.log(`拆分hashStr - part ${i}:`, hashStr.substring(parts[i][0], parts[i][0] + parts[i][1]));
|
||||||
|
const [start, length] = parts[i];
|
||||||
|
const hexStr = hashStr.substring(start, start + length);
|
||||||
|
const intValue = parseInt(hexStr, 16);
|
||||||
|
// console.log(`转换为整数 - part ${i}:`, intValue);
|
||||||
|
ints[4 - i] = intValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 获取调用接口时间戳(毫秒)并进行去重处理
|
||||||
|
const now = timestamp || Date.now();
|
||||||
|
|
||||||
|
// 对时间戳进行去重处理(处理同一毫秒内相同接口的重复请求)
|
||||||
|
const adjustedTimestamp = getDeduplicatedTimestamp(now, method, urlPath);
|
||||||
|
const ts = adjustedTimestamp.toString();
|
||||||
|
|
||||||
|
// 将时间拆分为两部分
|
||||||
|
const t0 = parseInt(ts.substring(0, ts.length - 9), 10);
|
||||||
|
const t1 = parseInt(ts.substring(ts.length - 9), 10);
|
||||||
|
|
||||||
|
ints[2] += t0;
|
||||||
|
ints[5] = t1;
|
||||||
|
|
||||||
|
// 记录实际使用的时间戳,用于后续的重复检测
|
||||||
|
lastUsedTimestamp = adjustedTimestamp;
|
||||||
|
|
||||||
|
// console.log('时间戳:', now, '调整后:', adjustedTimestamp, '拆分:', { t0, t1 });
|
||||||
|
|
||||||
|
// 9. 使用hashids算法加密
|
||||||
|
const alphabet = "GxJRk37QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfNhobIcMvKt";
|
||||||
|
const hashids = new Hashids(salt, 0, alphabet);
|
||||||
|
|
||||||
|
// 将ints数组编码为字符串
|
||||||
|
const sign = hashids.encode(ints);
|
||||||
|
|
||||||
|
// 打印调试信息
|
||||||
|
// console.log('=== Sign生成调试信息 ===');
|
||||||
|
// console.log('udid:', udid);
|
||||||
|
// console.log('signSalt:', signSalt);
|
||||||
|
// console.log('生成的salt:', salt);
|
||||||
|
// console.log('参数映射:', Object.fromEntries(reqMap));
|
||||||
|
// console.log('排序后的keys:', sortedKeys);
|
||||||
|
// console.log('拼接字符串:', contentStr);
|
||||||
|
// console.log('HMAC-MD5哈希:', hashStr);
|
||||||
|
// console.log('分割后的ints:', ints);
|
||||||
|
// console.log('原始时间戳:', now, '调整后时间戳:', adjustedTimestamp, '拆分:', { t0, t1 });
|
||||||
|
// console.log('最终sign:', sign);
|
||||||
|
// console.log('========================');
|
||||||
|
|
||||||
|
return sign;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成sign失败:', error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置去重状态(可选,用于测试或特殊场景)
|
||||||
|
*/
|
||||||
|
export function resetSignDeduplication() {
|
||||||
|
lastRequestKey = '';
|
||||||
|
lastTimestamp = 0;
|
||||||
|
lastUsedTimestamp = 0;
|
||||||
|
collisionCount = 0;
|
||||||
|
console.log('Sign去重状态已重置');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成salt - 匹配后端的genSalt函数
|
||||||
|
* 后端逻辑: GenTokenSalt(uid, udid + signSalt)
|
||||||
|
*
|
||||||
|
* @param {string} shortUdid - 22位短udid
|
||||||
|
* @param {string} signSalt - 固定的signSalt值 'xspotter-s3'
|
||||||
|
* @returns {Promise<string>} salt值
|
||||||
|
*/
|
||||||
|
async function generateSalt(shortUdid, signSalt) {
|
||||||
|
try {
|
||||||
|
// 1. 将22位短udid解压为完整UUID
|
||||||
|
const fullUdid = await unpackUUID(shortUdid);
|
||||||
|
|
||||||
|
// 2. 解析UUID并提取每个部分的第一个字符
|
||||||
|
// UUID格式: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
const uuidParts = fullUdid.split('-');
|
||||||
|
if (uuidParts.length !== 5) {
|
||||||
|
throw new Error('Invalid UUID format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取每个部分的第一个字符组成hexStr
|
||||||
|
// 后端: for i, item := range strings.Split(udidStr, "-") { hexStr += string(item[i]) }
|
||||||
|
let hexStr = '';
|
||||||
|
for (let i = 0; i < uuidParts.length; i++) {
|
||||||
|
if (uuidParts[i].length > i) {
|
||||||
|
hexStr += uuidParts[i][i]; // 注意:取第i个字符,不是第一个
|
||||||
|
} else {
|
||||||
|
hexStr += uuidParts[i][0]; // 如果长度不够,取第一个
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 将hexStr解析为整数(16进制)
|
||||||
|
const baseInt = parseInt(hexStr, 16);
|
||||||
|
|
||||||
|
// 4. 转换为8进制字符串
|
||||||
|
const base8Str = baseInt.toString(8);
|
||||||
|
|
||||||
|
// 5. secret = udid + signSalt
|
||||||
|
const secret = shortUdid + signSalt;
|
||||||
|
|
||||||
|
// 6. 返回 base8 + secret
|
||||||
|
const result = base8Str + secret;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成salt失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将22位短udid解压为完整UUID
|
||||||
|
* 匹配后端的unpackUUID函数
|
||||||
|
*/
|
||||||
|
async function unpackUUID(shortUdid) {
|
||||||
|
try {
|
||||||
|
// 后端逻辑: shortUdid + "==" 然后base64解码
|
||||||
|
const base64Str = shortUdid + '==';
|
||||||
|
|
||||||
|
// 将url安全的base64转换为标准base64
|
||||||
|
const standardBase64 = base64Str.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
|
||||||
|
// base64解码
|
||||||
|
const binaryStr = atob(standardBase64);
|
||||||
|
|
||||||
|
// 转换为16进制格式
|
||||||
|
const hex = [];
|
||||||
|
for (let i = 0; i < binaryStr.length; i++) {
|
||||||
|
const hexByte = binaryStr.charCodeAt(i).toString(16).padStart(2, '0');
|
||||||
|
hex.push(hexByte);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化为UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
const hexStr = hex.join('');
|
||||||
|
const uuid = `${hexStr.substring(0, 8)}-${hexStr.substring(8, 12)}-${hexStr.substring(12, 16)}-${hexStr.substring(16, 20)}-${hexStr.substring(20, 32)}`;
|
||||||
|
|
||||||
|
return uuid;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解压UUID失败:', error);
|
||||||
|
// 如果已经是完整UUID格式,直接返回
|
||||||
|
if (shortUdid.includes('-') && shortUdid.length === 36) {
|
||||||
|
return shortUdid;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将完整UUID压缩为22位短udid
|
||||||
|
* 匹配后端的packUDID函数
|
||||||
|
*/
|
||||||
|
export function packUUID(fullUuid) {
|
||||||
|
try {
|
||||||
|
// 移除UUID中的连字符
|
||||||
|
const hexStr = fullUuid.replace(/-/g, '');
|
||||||
|
|
||||||
|
// 将16进制字符串转换为字节数组
|
||||||
|
const bytes = [];
|
||||||
|
for (let i = 0; i < hexStr.length; i += 2) {
|
||||||
|
bytes.push(parseInt(hexStr.substring(i, i + 2), 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为二进制字符串
|
||||||
|
let binary = '';
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// base64编码并转换为url安全格式
|
||||||
|
const base64 = btoa(binary);
|
||||||
|
const base64url = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||||
|
|
||||||
|
return base64url;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('压缩UUID失败:', error);
|
||||||
|
return fullUuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,39 +39,20 @@
|
|||||||
|
|
||||||
<h2 class="login-title">系统登录</h2>
|
<h2 class="login-title">系统登录</h2>
|
||||||
|
|
||||||
<el-form
|
<el-form ref="loginRef" class="login-form" :model="loginForm" :rules="loginRules">
|
||||||
ref="loginRef"
|
|
||||||
class="login-form"
|
|
||||||
:model="loginForm"
|
|
||||||
:rules="loginRules"
|
|
||||||
>
|
|
||||||
|
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="username">
|
||||||
<el-input
|
<el-input v-model="loginForm.username" placeholder="请输入账号" size="large" />
|
||||||
v-model="loginForm.username"
|
|
||||||
placeholder="请输入账号"
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="password">
|
<el-form-item prop="password">
|
||||||
<el-input
|
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码" size="large"
|
||||||
v-model="loginForm.password"
|
@keyup.enter="handleLogin" />
|
||||||
type="password"
|
|
||||||
placeholder="请输入密码"
|
|
||||||
size="large"
|
|
||||||
@keyup.enter="handleLogin"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button
|
<el-button :loading="loading" class="login-button" type="primary" size="large"
|
||||||
:loading="loading"
|
@click.prevent="handleLogin">
|
||||||
class="login-button"
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
@click.prevent="handleLogin"
|
|
||||||
>
|
|
||||||
<span v-if="!loading">登 录</span>
|
<span v-if="!loading">登 录</span>
|
||||||
<span v-else>登录中...</span>
|
<span v-else>登录中...</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -129,6 +110,7 @@ function handleLogin() {
|
|||||||
proxy.$refs.loginRef.validate((valid) => {
|
proxy.$refs.loginRef.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
meterStore.initUdid()
|
||||||
if (!localStorage?.getItem('UDID')) {
|
if (!localStorage?.getItem('UDID')) {
|
||||||
ElMessage({
|
ElMessage({
|
||||||
message: '服务错误,请刷新页面',
|
message: '服务错误,请刷新页面',
|
||||||
@@ -219,13 +201,12 @@ function requestNotificationPermission() {
|
|||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
meterStore.initUdid()
|
// meterStore.initUdid()
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
.loginView {
|
.loginView {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
@@ -240,9 +221,9 @@ onMounted(async () => {
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
|
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 20% 20%, rgba(64,158,255,.15), transparent 40%),
|
radial-gradient(circle at 20% 20%, rgba(64, 158, 255, .15), transparent 40%),
|
||||||
radial-gradient(circle at 80% 80%, rgba(64,158,255,.15), transparent 40%),
|
radial-gradient(circle at 80% 80%, rgba(64, 158, 255, .15), transparent 40%),
|
||||||
linear-gradient(120deg,#f6f9fc,#eef3ff);
|
linear-gradient(120deg, #f6f9fc, #eef3ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 动态光效 */
|
/* 动态光效 */
|
||||||
@@ -254,11 +235,9 @@ onMounted(async () => {
|
|||||||
width: 600px;
|
width: 600px;
|
||||||
height: 600px;
|
height: 600px;
|
||||||
|
|
||||||
background: radial-gradient(
|
background: radial-gradient(circle,
|
||||||
circle,
|
rgba(64, 158, 255, .25),
|
||||||
rgba(64,158,255,.25),
|
transparent 70%);
|
||||||
transparent 70%
|
|
||||||
);
|
|
||||||
|
|
||||||
filter: blur(80px);
|
filter: blur(80px);
|
||||||
|
|
||||||
@@ -268,11 +247,11 @@ onMounted(async () => {
|
|||||||
@keyframes floatLight {
|
@keyframes floatLight {
|
||||||
|
|
||||||
from {
|
from {
|
||||||
transform: translate(-200px,-100px);
|
transform: translate(-200px, -100px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
transform: translate(300px,200px);
|
transform: translate(300px, 200px);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -302,12 +281,12 @@ onMounted(async () => {
|
|||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
|
||||||
background: rgba(255,255,255,.9);
|
background: rgba(255, 255, 255, .9);
|
||||||
|
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 20px 50px rgba(0,0,0,.08);
|
0 20px 50px rgba(0, 0, 0, .08);
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -345,7 +324,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.feature-list {
|
.feature-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2,1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,11 +382,9 @@ onMounted(async () => {
|
|||||||
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
background: linear-gradient(
|
background: linear-gradient(135deg,
|
||||||
135deg,
|
|
||||||
#409EFF,
|
#409EFF,
|
||||||
#66b1ff
|
#66b1ff);
|
||||||
);
|
|
||||||
|
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
@@ -416,7 +393,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
|
|
||||||
box-shadow: 0 6px 16px rgba(64,158,255,.25);
|
box-shadow: 0 6px 16px rgba(64, 158, 255, .25);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端 */
|
/* 移动端 */
|
||||||
@@ -442,5 +419,4 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { defineConfig ,loadEnv} from 'vite'
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import createVitePlugins from './vite/plugins/index.js'
|
import createVitePlugins from './vite/plugins/index.js'
|
||||||
|
|
||||||
@@ -11,6 +11,10 @@ export default defineConfig(({ mode, command }) => {
|
|||||||
return {
|
return {
|
||||||
//生产环境使用相对路径,开发环境用 /
|
//生产环境使用相对路径,开发环境用 /
|
||||||
base: command === 'build' ? './' : '/',
|
base: command === 'build' ? './' : '/',
|
||||||
|
build: {
|
||||||
|
outDir: 'xSynergy-manage',
|
||||||
|
},
|
||||||
|
|
||||||
plugins: createVitePlugins(env, command === "build"),
|
plugins: createVitePlugins(env, command === "build"),
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0', // 关键配置,允许局域网访问
|
host: '0.0.0.0', // 关键配置,允许局域网访问
|
||||||
|
|||||||
Reference in New Issue
Block a user