✨ 新增功能: - 全局路由守卫模块 (route-guard.js) - 多层次权限验证 (登录状态+Token存在+Token过期检查) - 自动拦截机制 (页面级+跳转级+HTTP级+应用级) - 401响应自动处理和跳转 - 路由守卫测试页面 🔧 技术实现: - checkPageAccess() 全局页面权限检查 - guardedNavigate/guardedSwitchTab 带权限检查的跳转 - handleUnauthorized() HTTP拦截器401处理 - 公开页面配置管理 - 优雅的错误提示和跳转逻辑 📱 页面集成: - App.vue 初始化路由守卫 - profile/workbench 页面添加权限检查 - index 页面优化跳转逻辑 - 新增测试页面验证功能 📚 文档完善: - 移动端路由守卫使用指南 - 详细的变更记录和技术说明 - 最佳实践和注意事项 🎯 核心效果: 用户登录失效或未登录时,访问任何需要权限的页面都自动跳转到登录页,提升安全性和用户体验。
200 lines
5.8 KiB
JavaScript
200 lines
5.8 KiB
JavaScript
// @ts-nocheck
|
|
import CryptoJS from 'crypto-js';
|
|
|
|
// 配置信息
|
|
const ENCRYPT_CONFIG = {
|
|
// RSA公钥 - 与后端保持一致 (更新为正确版本,包含ijT7h)
|
|
RSA_PUBLIC_KEY:
|
|
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
|
|
CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
|
TENANT_ID: '000000',
|
|
};
|
|
|
|
/**
|
|
* 生成随机AES密钥
|
|
* @param {number} length 密钥长度,默认32位
|
|
* @returns {string} AES密钥
|
|
*/
|
|
export function generateRandomAESKey(length = 32) {
|
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
let result = '';
|
|
for (let i = 0; i < length; i++) {
|
|
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* AES加密数据
|
|
* @param {string} data 待加密的数据
|
|
* @param {string} key AES密钥
|
|
* @returns {string} 加密后的数据
|
|
*/
|
|
export function encryptWithAES(data, key) {
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* Base64字符串转ArrayBuffer
|
|
* @param {string} base64
|
|
* @returns {ArrayBuffer}
|
|
*/
|
|
function base64ToArrayBuffer(base64) {
|
|
const binaryString = atob(base64);
|
|
const bytes = new Uint8Array(binaryString.length);
|
|
for (let i = 0; i < binaryString.length; i++) {
|
|
bytes[i] = binaryString.charCodeAt(i);
|
|
}
|
|
return bytes.buffer;
|
|
}
|
|
|
|
/**
|
|
* ArrayBuffer转Base64字符串
|
|
* @param {ArrayBuffer} buffer
|
|
* @returns {string}
|
|
*/
|
|
function arrayBufferToBase64(buffer) {
|
|
const bytes = new Uint8Array(buffer);
|
|
let binary = '';
|
|
for (let i = 0; i < bytes.byteLength; i++) {
|
|
binary += String.fromCharCode(bytes[i]);
|
|
}
|
|
return btoa(binary);
|
|
}
|
|
|
|
/**
|
|
* 导入RSA公钥
|
|
* @param {string} publicKeyBase64 Base64格式的公钥
|
|
* @returns {Promise<CryptoKey>}
|
|
*/
|
|
async function importRSAPublicKey(publicKeyBase64) {
|
|
try {
|
|
const keyBuffer = base64ToArrayBuffer(publicKeyBase64);
|
|
|
|
const key = await crypto.subtle.importKey(
|
|
'spki',
|
|
keyBuffer,
|
|
{
|
|
name: 'RSA-OAEP',
|
|
hash: 'SHA-1',
|
|
},
|
|
false,
|
|
['encrypt'],
|
|
);
|
|
|
|
return key;
|
|
} catch (error) {
|
|
console.error('导入RSA公钥失败:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* RSA加密AES密钥 - 使用Web Crypto API
|
|
* @param {string} aesKey AES密钥
|
|
* @returns {Promise<string|null>} RSA加密后的AES密钥
|
|
*/
|
|
export async function encryptAESKeyWithRSA(aesKey) {
|
|
try {
|
|
// 检查Web Crypto API支持
|
|
if (!crypto || !crypto.subtle) {
|
|
throw new Error('当前环境不支持Web Crypto API');
|
|
}
|
|
|
|
// 导入RSA公钥
|
|
const publicKey = await importRSAPublicKey(ENCRYPT_CONFIG.RSA_PUBLIC_KEY);
|
|
|
|
// 将AES密钥转为Base64(与custom-login-script.js保持一致)
|
|
const aesKeyBase64 = btoa(unescape(encodeURIComponent(aesKey)));
|
|
|
|
// 将字符串转为ArrayBuffer
|
|
const dataBuffer = new TextEncoder().encode(aesKeyBase64);
|
|
|
|
// RSA加密
|
|
const encryptedBuffer = await crypto.subtle.encrypt(
|
|
{
|
|
name: 'RSA-OAEP',
|
|
},
|
|
publicKey,
|
|
dataBuffer,
|
|
);
|
|
|
|
// 转为Base64字符串
|
|
const encryptedBase64 = arrayBufferToBase64(encryptedBuffer);
|
|
|
|
console.log('✅ Web Crypto RSA加密成功,结果长度:', encryptedBase64.length);
|
|
return encryptedBase64;
|
|
} catch (error) {
|
|
console.error('Web Crypto RSA加密失败:', error.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 登录数据加密处理
|
|
* @param {Object} loginData 登录数据对象
|
|
* @returns {Promise<Object>} 包含加密数据和请求头的对象
|
|
*/
|
|
export async function encryptLoginData(loginData) {
|
|
try {
|
|
// 1. 生成AES密钥
|
|
const aesKey = generateRandomAESKey(32);
|
|
console.log('🔑 生成AES密钥:', aesKey);
|
|
|
|
// 2. 准备完整的登录数据
|
|
const fullLoginData = {
|
|
tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
|
|
username: loginData.username,
|
|
password: loginData.password,
|
|
clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
|
|
grantType: loginData.grantType || 'password',
|
|
};
|
|
|
|
console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
|
|
|
|
// 3. AES加密登录数据
|
|
const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
|
|
console.log('🔐 AES加密后数据长度:', encryptedData.length);
|
|
|
|
// 4. RSA加密AES密钥 - 异步操作
|
|
const encryptedKey = await encryptAESKeyWithRSA(aesKey);
|
|
if (!encryptedKey) {
|
|
throw new Error('RSA加密AES密钥失败');
|
|
}
|
|
console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
|
|
|
|
// 5. 返回加密结果和请求头
|
|
return {
|
|
success: true,
|
|
encryptedData: encryptedData,
|
|
headers: {
|
|
'Content-Type': 'application/json;charset=utf-8',
|
|
clientid: ENCRYPT_CONFIG.CLIENT_ID,
|
|
'encrypt-key': encryptedKey,
|
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
|
isToken: 'false',
|
|
isEncrypt: 'true',
|
|
repeatSubmit: 'false',
|
|
},
|
|
};
|
|
} catch (error) {
|
|
console.error('登录数据加密失败:', error);
|
|
return {
|
|
success: false,
|
|
error: error.message,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取加密配置
|
|
*/
|
|
export function getEncryptConfig() {
|
|
return ENCRYPT_CONFIG;
|
|
}
|