处理vue页面登录失败问题
This commit is contained in:
parent
296e6d74df
commit
174f8454c3
@ -238,11 +238,9 @@ const router = createRouter({
|
|||||||
// 已添加的动态路由
|
// 已添加的动态路由
|
||||||
const addedRouteNames = new Set<string>()
|
const addedRouteNames = new Set<string>()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据菜单动态添加新路由(只添加静态路由中没有的)
|
* 根据菜单动态添加新路由(只添加静态路由中没有的)
|
||||||
*/
|
*/
|
||||||
export function addDynamicRoutes(menus: any) { //MenuInfo[]
|
export function addDynamicRoutes(menus: any) { //MenuInfo[]
|
||||||
//console.log('[动态路由] 开始处理菜单:', menus)
|
//console.log('[动态路由] 开始处理菜单:', menus)
|
||||||
const addRoutes = (menuList: any) => { //MenuInfo[]
|
const addRoutes = (menuList: any) => { //MenuInfo[]
|
||||||
@ -250,22 +248,22 @@ export function addDynamicRoutes(menus: any) { //MenuInfo[]
|
|||||||
// 只处理菜单类型(type=2),且有 path
|
// 只处理菜单类型(type=2),且有 path
|
||||||
if (menu.type === 2 && menu.path) {
|
if (menu.type === 2 && menu.path) {
|
||||||
const routeName = 'Dynamic-' + menu.id
|
const routeName = 'Dynamic-' + menu.id
|
||||||
|
|
||||||
// 检查是否已经有同路径的静态路由
|
// 检查是否已经有同路径的静态路由
|
||||||
const existingRoutes = router.getRoutes()
|
const existingRoutes = router.getRoutes()
|
||||||
const menuPath = menu.path.startsWith('/') ? menu.path.slice(1) : menu.path
|
const menuPath = menu.path.startsWith('/') ? menu.path.slice(1) : menu.path
|
||||||
const pathExists = existingRoutes.some(r => r.path === '/' + menuPath || r.path === menuPath)
|
const pathExists = existingRoutes.some(r => r.path === '/' + menuPath || r.path === menuPath)
|
||||||
|
|
||||||
if (pathExists) {
|
if (pathExists) {
|
||||||
// console.log(`[动态路由] 跳过(已存在): ${menuPath}`)
|
// console.log(`[动态路由] 跳过(已存在): ${menuPath}`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addedRouteNames.has(routeName)) {
|
if (addedRouteNames.has(routeName)) {
|
||||||
//console.log(`[动态路由] 跳过(已添加): ${menuPath}`)
|
//console.log(`[动态路由] 跳过(已添加): ${menuPath}`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否是外链菜单
|
// 判断是否是外链菜单
|
||||||
if (menu.isFrame === 1 && menu.component) {
|
if (menu.isFrame === 1 && menu.component) {
|
||||||
// 外链菜单,使用 iframe 组件
|
// 外链菜单,使用 iframe 组件
|
||||||
@ -287,9 +285,9 @@ export function addDynamicRoutes(menus: any) { //MenuInfo[]
|
|||||||
const componentName = menu.component.startsWith('/') ? menu.component.slice(1) : menu.component
|
const componentName = menu.component.startsWith('/') ? menu.component.slice(1) : menu.component
|
||||||
const componentPath = `/src/views/${componentName}.vue`
|
const componentPath = `/src/views/${componentName}.vue`
|
||||||
const component = modules[componentPath]
|
const component = modules[componentPath]
|
||||||
|
|
||||||
//console.log(`[动态路由] 处理: path=${menuPath}, component=${componentPath}, 组件存在=${!!component}`)
|
//console.log(`[动态路由] 处理: path=${menuPath}, component=${componentPath}, 组件存在=${!!component}`)
|
||||||
|
|
||||||
if (component) {
|
if (component) {
|
||||||
router.addRoute('Layout', {
|
router.addRoute('Layout', {
|
||||||
path: menuPath,
|
path: menuPath,
|
||||||
@ -308,14 +306,14 @@ export function addDynamicRoutes(menus: any) { //MenuInfo[]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 递归处理子菜单
|
// 递归处理子菜单
|
||||||
if (menu.children && menu.children.length > 0) {
|
if (menu.children && menu.children.length > 0) {
|
||||||
addRoutes(menu.children)
|
addRoutes(menu.children)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addRoutes(menus)
|
addRoutes(menus)
|
||||||
//console.log('[动态路由] 当前所有路由:', router.getRoutes().map(r => r.path))
|
//console.log('[动态路由] 当前所有路由:', router.getRoutes().map(r => r.path))
|
||||||
}
|
}
|
||||||
@ -337,7 +335,7 @@ export function resetRouter() {
|
|||||||
router.beforeEach(async (to, _from, next) => {
|
router.beforeEach(async (to, _from, next) => {
|
||||||
console.log('路由测试');
|
console.log('路由测试');
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
//document.title = `${to.meta.title || ''} - CSY Admin`
|
//document.title = `${to.meta.title || ''} - CSY Admin`
|
||||||
if (to.meta.requiresAuth === false) {
|
if (to.meta.requiresAuth === false) {
|
||||||
next()
|
next()
|
||||||
@ -372,7 +370,7 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if (!userStore.user) {
|
// if (!userStore.user) {
|
||||||
// try {
|
// try {
|
||||||
// await userStore.getInfo()
|
// await userStore.getInfo()
|
||||||
// // 添加动态路由(只添加新的,不影响已有的)
|
// // 添加动态路由(只添加新的,不影响已有的)
|
||||||
// addDynamicRoutes(userStore.menus)
|
// addDynamicRoutes(userStore.menus)
|
||||||
@ -389,7 +387,7 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
// next()
|
// next()
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
@ -1,4 +1,7 @@
|
|||||||
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
|
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
// const BASE_API = import.meta.env.VITE_APP_BASE_API
|
||||||
|
|
||||||
// API响应结构
|
// API响应结构
|
||||||
interface ApiResponse<T = any> {
|
interface ApiResponse<T = any> {
|
||||||
@ -11,24 +14,12 @@ interface ApiResponse<T = any> {
|
|||||||
interface CryptoConfig {
|
interface CryptoConfig {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
publicKey: string
|
publicKey: string
|
||||||
aesKey: string
|
aesKey: string // AES密钥的Base64编码
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加密配置缓存
|
// 加密配置缓存
|
||||||
let cryptoConfigCache: CryptoConfig | null = null
|
let cryptoConfigCache: CryptoConfig | null = null
|
||||||
|
|
||||||
// 从 localStorage 安全读取 token(避免循环依赖)
|
|
||||||
function getToken(): string | null {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem('mes-user')
|
|
||||||
if (raw) {
|
|
||||||
const data = JSON.parse(raw)
|
|
||||||
return data?.token || null
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取加密配置
|
// 获取加密配置
|
||||||
export async function fetchCryptoConfig(): Promise<CryptoConfig> {
|
export async function fetchCryptoConfig(): Promise<CryptoConfig> {
|
||||||
if (cryptoConfigCache) {
|
if (cryptoConfigCache) {
|
||||||
@ -51,7 +42,7 @@ export function clearCryptoConfigCache() {
|
|||||||
cryptoConfigCache = null
|
cryptoConfigCache = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否是AES加密的响应数据
|
// 判断是否是AES加密的响应数据(格式:iv.encryptedData)
|
||||||
function isAesEncryptedData(data: any): boolean {
|
function isAesEncryptedData(data: any): boolean {
|
||||||
if (typeof data !== 'string') {
|
if (typeof data !== 'string') {
|
||||||
return false
|
return false
|
||||||
@ -60,9 +51,11 @@ function isAesEncryptedData(data: any): boolean {
|
|||||||
if (parts.length !== 2) {
|
if (parts.length !== 2) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
// 检查两部分是否都是有效的Base64,且IV长度正确
|
||||||
try {
|
try {
|
||||||
atob(parts[0])
|
atob(parts[0])
|
||||||
atob(parts[1])
|
atob(parts[1])
|
||||||
|
// IV是12字节,Base64后是16字符
|
||||||
return parts[0].length === 16 && parts[1].length > 10
|
return parts[0].length === 16 && parts[1].length > 10
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
@ -80,6 +73,7 @@ async function aesDecrypt(encryptedData: string, aesKeyBase64: string): Promise<
|
|||||||
const data = Uint8Array.from(atob(parts[1]), c => c.charCodeAt(0))
|
const data = Uint8Array.from(atob(parts[1]), c => c.charCodeAt(0))
|
||||||
const keyBytes = Uint8Array.from(atob(aesKeyBase64), c => c.charCodeAt(0))
|
const keyBytes = Uint8Array.from(atob(aesKeyBase64), c => c.charCodeAt(0))
|
||||||
|
|
||||||
|
// 导入AES密钥
|
||||||
const aesKey = await crypto.subtle.importKey(
|
const aesKey = await crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
keyBytes,
|
keyBytes,
|
||||||
@ -88,6 +82,7 @@ async function aesDecrypt(encryptedData: string, aesKeyBase64: string): Promise<
|
|||||||
['decrypt']
|
['decrypt']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 解密
|
||||||
const decrypted = await crypto.subtle.decrypt(
|
const decrypted = await crypto.subtle.decrypt(
|
||||||
{ name: 'AES-GCM', iv: iv },
|
{ name: 'AES-GCM', iv: iv },
|
||||||
aesKey,
|
aesKey,
|
||||||
@ -110,24 +105,36 @@ async function decryptResponseData(data: string): Promise<any> {
|
|||||||
return JSON.parse(decryptedStr)
|
return JSON.parse(decryptedStr)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('响应解密失败', error)
|
console.error('响应解密失败', error)
|
||||||
|
// 解密失败可能是密钥过期,清除缓存
|
||||||
cryptoConfigCache = null
|
cryptoConfigCache = null
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//axios.defaults.headers["Content-Type"] = 'application/json;charset=utf-8' //"application/x-www-form-urlencoded;charset=utf-8"; //"application/json;charset=utf-8";
|
||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
|
// 后端 API 统一使用 /api 前缀
|
||||||
const service: AxiosInstance = axios.create({
|
const service: AxiosInstance = axios.create({
|
||||||
baseURL: '/api',
|
baseURL:'/api',
|
||||||
timeout: 30000
|
timeout: 30000
|
||||||
})
|
})
|
||||||
|
|
||||||
// 请求拦截器 - 直接从 localStorage 读取 token
|
// 请求拦截器
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
const token = getToken()
|
const userStore = useUserStore()
|
||||||
if (token) {
|
if (userStore.token) {
|
||||||
config.headers['Authorization'] = token
|
config.headers['Authorization'] = userStore.token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.data instanceof FormData) {
|
||||||
|
// 文件上传:不设置 JSON 请求头、不序列化,交给浏览器自动处理 multipart
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
config.headers["Content-Type"] = 'application/json;charset=utf-8'
|
||||||
|
config.data = config.data instanceof Object ? JSON.stringify(config.data) : config.data
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@ -141,25 +148,32 @@ let isLoggingOut = false
|
|||||||
// 响应拦截器
|
// 响应拦截器
|
||||||
service.interceptors.response.use(
|
service.interceptors.response.use(
|
||||||
async (response: AxiosResponse<ApiResponse>) => {
|
async (response: AxiosResponse<ApiResponse>) => {
|
||||||
// blob 类型响应直接返回
|
// 如果是 blob 类型响应(文件下载),直接返回
|
||||||
if (response.config.responseType === 'blob') {
|
if (response.config.responseType === 'blob') {
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.config.responseType === 'arraybuffer') {
|
||||||
|
// 二进制流不走code校验,直接完整返回ArrayBuffer
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const res = response.data
|
const res = response.data
|
||||||
|
|
||||||
if (res.code !== 200) {
|
if (res.code !== 200) {
|
||||||
|
// logout 接口返回 401 时不显示错误消息(避免干扰)
|
||||||
const isLogoutRequest = response.config.url?.includes('/auth/logout')
|
const isLogoutRequest = response.config.url?.includes('/auth/logout')
|
||||||
|
|
||||||
// 401 未授权
|
// 401 未授权,弹出明确提示并跳转登录(防止重复调用)
|
||||||
if (res.code === 401 && !isLoggingOut && !isLogoutRequest) {
|
if (res.code === 401 && !isLoggingOut && !isLogoutRequest) {
|
||||||
|
window.$message?.warning('当前用户登录已过期,请重新登录')
|
||||||
|
|
||||||
isLoggingOut = true
|
isLoggingOut = true
|
||||||
window.$message?.error('当前用户登录已过期,请重新登录')
|
|
||||||
// 清除本地 token
|
const userStore = useUserStore()
|
||||||
localStorage.removeItem('mes-user')
|
await userStore.logout()
|
||||||
isLoggingOut = false
|
isLoggingOut = false
|
||||||
// 跳转到登录页
|
|
||||||
window.location.href = '/login'
|
|
||||||
return Promise.reject(new Error('登录已过期'))
|
return Promise.reject(new Error('登录已过期'))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,7 +184,7 @@ service.interceptors.response.use(
|
|||||||
return Promise.reject(new Error(res.message || '请求失败'))
|
return Promise.reject(new Error(res.message || '请求失败'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// AES 解密
|
// 检查响应数据是否是AES加密的,自动解密
|
||||||
if (isAesEncryptedData(res.data)) {
|
if (isAesEncryptedData(res.data)) {
|
||||||
try {
|
try {
|
||||||
return await decryptResponseData(res.data)
|
return await decryptResponseData(res.data)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user