处理vue页面登录失败问题

This commit is contained in:
andy 2026-08-04 14:50:51 +08:00
parent 296e6d74df
commit 174f8454c3
2 changed files with 54 additions and 42 deletions

View File

@ -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

View File

@ -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)