feat:添加接口签名
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ lerna-debug.log*
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
xSynergy-manage
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
21
README.md
21
README.md
@@ -27,3 +27,24 @@ npm run dev
|
||||
```sh
|
||||
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",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"axios": "^0.27.2",
|
||||
"blakejs": "^1.2.1",
|
||||
"code-inspector-plugin": "^0.20.12",
|
||||
"crypto-js": "^4.2.0",
|
||||
"element-plus": "^2.2.27",
|
||||
"hashids": "^1.2.2",
|
||||
"js-cookie": "^3.0.1",
|
||||
"livekit-client": "^2.7.5",
|
||||
"mitt": "^3.0.0",
|
||||
@@ -2144,6 +2146,11 @@
|
||||
"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": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz",
|
||||
@@ -4563,6 +4570,11 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
"@vueuse/core": "^9.5.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"axios": "^0.27.2",
|
||||
"blakejs": "^1.2.1",
|
||||
"code-inspector-plugin": "^0.20.12",
|
||||
"crypto-js": "^4.2.0",
|
||||
"element-plus": "^2.2.27",
|
||||
"hashids": "^1.2.2",
|
||||
"js-cookie": "^3.0.1",
|
||||
"livekit-client": "^2.7.5",
|
||||
"mitt": "^3.0.0",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import request from '@/utils/request'
|
||||
// import request from '@/views/custom/Meter/public/request.js'
|
||||
|
||||
// 登录方法
|
||||
export function login(username, password) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ElMessage,
|
||||
} from "element-plus";
|
||||
import { tansParams } from "@/utils/ruoyi";
|
||||
import { generateSign } from "@/utils/tools";
|
||||
import cache from "@/plugins/cache";
|
||||
import { getToken, removeToken } from "@/utils/auth";
|
||||
import router from '@/router';
|
||||
@@ -38,7 +39,7 @@ const service = axios.create({
|
||||
|
||||
// request拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
async (config) => {
|
||||
// 在拦截器内部安全地使用 store
|
||||
let sudid = ''
|
||||
try {
|
||||
@@ -66,7 +67,6 @@ service.interceptors.request.use(
|
||||
}
|
||||
// get请求映射params参数
|
||||
if (config.method === "get" && config.params) {
|
||||
|
||||
let url = config.url + "?" + tansParams(config.params);
|
||||
url = url.slice(0, -1);
|
||||
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;
|
||||
},
|
||||
(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(
|
||||
(response) => {
|
||||
try {
|
||||
// console.log('原始响应:', response);
|
||||
// 1. 检查响应是否存在
|
||||
if (!response) {
|
||||
ElMessage.error('无响应数据');
|
||||
@@ -145,6 +181,8 @@ service.interceptors.response.use(
|
||||
return responseData;
|
||||
}
|
||||
|
||||
// console.log('响应数据:', responseData);
|
||||
|
||||
// 4. 根据业务码处理不同情况
|
||||
switch (businessCode) {
|
||||
case 200:
|
||||
@@ -162,35 +200,97 @@ service.interceptors.response.use(
|
||||
// });
|
||||
// }
|
||||
case 500:
|
||||
const serverErrorMsg = responseData.meta?.message || '服务器内部错误';
|
||||
const serverErrorMsg = responseData.meta?.message || responseData.meta?.msg || '服务器内部错误';
|
||||
ElMessage({ message: serverErrorMsg, type: 'error' });
|
||||
return Promise.reject({ code: 500, message: serverErrorMsg });
|
||||
|
||||
default:
|
||||
const errorMsg = responseData.meta?.message || `业务错误 (${businessCode})`;
|
||||
const errorMsg = responseData.meta?.message || responseData.meta?.msg || `业务错误 (${businessCode})`;
|
||||
ElNotification.error({ title: 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) => {
|
||||
let { message } = error;
|
||||
try {
|
||||
let message = error?.message || '请求失败';
|
||||
let code = error?.response?.status || -1;
|
||||
if (message == 'Network Error') {
|
||||
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) + '异常';
|
||||
}
|
||||
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({
|
||||
code,
|
||||
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
|
||||
export function generateUUID() {
|
||||
var d = new Date().getTime();
|
||||
@@ -158,3 +162,429 @@ function isValidBox(x1, y1, x2, y2, imgWidth, 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>
|
||||
|
||||
<el-form
|
||||
ref="loginRef"
|
||||
class="login-form"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
>
|
||||
<el-form ref="loginRef" class="login-form" :model="loginForm" :rules="loginRules">
|
||||
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="请输入账号"
|
||||
size="large"
|
||||
/>
|
||||
<el-input v-model="loginForm.username" placeholder="请输入账号" size="large" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
size="large"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码" size="large"
|
||||
@keyup.enter="handleLogin" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
:loading="loading"
|
||||
class="login-button"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click.prevent="handleLogin"
|
||||
>
|
||||
<el-button :loading="loading" class="login-button" type="primary" size="large"
|
||||
@click.prevent="handleLogin">
|
||||
<span v-if="!loading">登 录</span>
|
||||
<span v-else>登录中...</span>
|
||||
</el-button>
|
||||
@@ -129,6 +110,7 @@ function handleLogin() {
|
||||
proxy.$refs.loginRef.validate((valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
meterStore.initUdid()
|
||||
if (!localStorage?.getItem('UDID')) {
|
||||
ElMessage({
|
||||
message: '服务错误,请刷新页面',
|
||||
@@ -219,13 +201,12 @@ function requestNotificationPermission() {
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
meterStore.initUdid()
|
||||
// meterStore.initUdid()
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.loginView {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
@@ -240,9 +221,9 @@ onMounted(async () => {
|
||||
inset: 0;
|
||||
|
||||
background:
|
||||
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%),
|
||||
linear-gradient(120deg,#f6f9fc,#eef3ff);
|
||||
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%),
|
||||
linear-gradient(120deg, #f6f9fc, #eef3ff);
|
||||
}
|
||||
|
||||
/* 动态光效 */
|
||||
@@ -254,11 +235,9 @@ onMounted(async () => {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(64,158,255,.25),
|
||||
transparent 70%
|
||||
);
|
||||
background: radial-gradient(circle,
|
||||
rgba(64, 158, 255, .25),
|
||||
transparent 70%);
|
||||
|
||||
filter: blur(80px);
|
||||
|
||||
@@ -268,11 +247,11 @@ onMounted(async () => {
|
||||
@keyframes floatLight {
|
||||
|
||||
from {
|
||||
transform: translate(-200px,-100px);
|
||||
transform: translate(-200px, -100px);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(300px,200px);
|
||||
transform: translate(300px, 200px);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -302,12 +281,12 @@ onMounted(async () => {
|
||||
|
||||
border-radius: 16px;
|
||||
|
||||
background: rgba(255,255,255,.9);
|
||||
background: rgba(255, 255, 255, .9);
|
||||
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
box-shadow:
|
||||
0 20px 50px rgba(0,0,0,.08);
|
||||
0 20px 50px rgba(0, 0, 0, .08);
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -345,7 +324,7 @@ onMounted(async () => {
|
||||
|
||||
.feature-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2,1fr);
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
@@ -403,11 +382,9 @@ onMounted(async () => {
|
||||
|
||||
border-radius: 6px;
|
||||
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
background: linear-gradient(135deg,
|
||||
#409EFF,
|
||||
#66b1ff
|
||||
);
|
||||
#66b1ff);
|
||||
|
||||
border: none;
|
||||
}
|
||||
@@ -416,7 +393,7 @@ onMounted(async () => {
|
||||
|
||||
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>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import path from "path";
|
||||
import { defineConfig ,loadEnv} from 'vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import createVitePlugins from './vite/plugins/index.js'
|
||||
|
||||
@@ -11,6 +11,10 @@ export default defineConfig(({ mode, command }) => {
|
||||
return {
|
||||
//生产环境使用相对路径,开发环境用 /
|
||||
base: command === 'build' ? './' : '/',
|
||||
build: {
|
||||
outDir: 'xSynergy-manage',
|
||||
},
|
||||
|
||||
plugins: createVitePlugins(env, command === "build"),
|
||||
server: {
|
||||
host: '0.0.0.0', // 关键配置,允许局域网访问
|
||||
|
||||
Reference in New Issue
Block a user