diff --git a/src/api/axiosConfig.js b/src/api/axiosConfig.js index f443dc3..8384c28 100644 --- a/src/api/axiosConfig.js +++ b/src/api/axiosConfig.js @@ -13,6 +13,9 @@ const axiosInstance = axios.create({ timeout: 10000 }); +// 并发锁:防止多个 401 同时触发重复 logout +let isHandlingUnauthorized = false; + export function setupInterceptors() { /** * 请求拦截器 @@ -50,7 +53,15 @@ export function setupInterceptors() { // 如果收到401错误,并且不是来自登录请求本身 if (error.response && error.response.status === 401 && originalRequest.url !== '/user/login') { - logoutUser(); // 调用简化的logoutUser,它只清除token,不导航 + if (!isHandlingUnauthorized) { + isHandlingUnauthorized = true; + try { + logoutUser(); // 清除token,路由守卫将处理跳转 + } finally { + // 在短时间后解除锁,避免长时间无法重新登录 + setTimeout(() => { isHandlingUnauthorized = false; }, 1000); + } + } } // 不需要额外的console.error,错误会自然地在调用处被捕获或显示在网络请求中 return Promise.reject(error); diff --git a/src/api/login.js b/src/api/login.js index 4430cbd..c7d529a 100644 --- a/src/api/login.js +++ b/src/api/login.js @@ -182,19 +182,3 @@ export const resetPassword = async (token, password) => { } } -/** - * 获取用户临时权限信息 - * 路由: /user/temp_privilege - * 方法: GET - * - * 需要登录 - * @returns {Promise} 返回一个包含临时权限信息的Promise对象 - */ -export const getTempPrivilege = async () => { - try { - const response = await axiosInstance.get('/user/temp_privilege'); - return response.data; - } catch (error) { - throw error; - } -} diff --git a/src/api/tournament.js b/src/api/tournament.js index 5cf7a1c..3df7cb6 100644 --- a/src/api/tournament.js +++ b/src/api/tournament.js @@ -1,51 +1,5 @@ import axiosInstance from './axiosConfig'; -// const API_BASE_URL = 'http://zybdatasupport.online:8000' // 不再需要 - -// // 创建 axios 实例 // 不再需要 -// const axiosInstance = axios.create({ -// baseURL: API_BASE_URL, -// headers: { -// 'Content-Type': 'application/json', -// 'Accept': 'application/json', -// 'X-Requested-With': 'XMLHttpRequest' -// }, -// timeout: 10000 -// }) - -// // 设置请求拦截器,自动添加 token // 不再需要 -// axiosInstance.interceptors.request.use( -// config => { -// const token = localStorage.getItem('access_token') -// if (token) { -// config.headers.Authorization = `Bearer ${token}` -// } -// return config -// }, -// error => { -// return Promise.reject(error) -// } -// ) - -// // 添加响应拦截器 // 不再需要 -// axiosInstance.interceptors.response.use( -// response => response, -// error => { -// if (error.response) { -// console.error('请求错误:', { -// status: error.response.status, -// data: error.response.data, -// config: error.config -// }) -// } else if (error.request) { -// console.error('网络错误:', error.request) -// } else { -// console.error('请求配置错误:', error.message) -// } -// return Promise.reject(error) -// } -// ) - /** * 添加赛事 * @param {Object} tournamentData - 赛事数据 diff --git a/src/router/index.js b/src/router/index.js index 49b10dd..24f9dc8 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -138,21 +138,7 @@ router.beforeEach(async (to, from, next) => { if (!requiresAuth && !requiredPrivilege) { return next(); // 如果页面不需要认证和权限,直接放行 } - - // 方案1: 检查SessionStorage中是否已有用户信息 (刷新后最先检查这里) - if (getStoredUser()) { - // 继续检查权限 - } - - // 方案2: 如果刚登录过,直接放行 - if (justLoggedIn.value) { - justLoggedIn.value = false; - // 此时token肯定有效,但可能还没来得及请求用户信息 - // 为确保用户信息被存储,可以调用一次,但不阻塞导航 - getUserInfo().catch(err => console.error("Error fetching user info after login:", err)); - } - - // 方案3: 检查localStorage中是否有token (刷新后SessionStorage没有用户信息时) + if (hasValidToken()) { try { await getUserInfo(); // 尝试获取用户信息,该函数会把用户信息存入SessionStorage @@ -164,16 +150,25 @@ router.beforeEach(async (to, from, next) => { path: '/backend/login', query: { redirect: to.fullPath, sessionExpired: 'true' } }); + // return next('/maps') } } + // 登录校验:如果页面需要登录但本地没有有效 token,则跳转登录页 + if (requiresAuth && !hasValidToken()) { + return next({ + path: '/maps', + query: { redirect: to.fullPath } + }) + } + // 权限校验 if (requiredPrivilege) { const user = getStoredUser(); if (!user || !hasPrivilegeWithTemp(user, requiredPrivilege)) { // 通过事件总线通知弹窗 eventBus.emit('no-privilege') - return next(false) + return next({ path: '/maps', replace: true }) } } diff --git a/src/utils/jwt.js b/src/utils/jwt.js index 5b5ae28..26cdbde 100644 --- a/src/utils/jwt.js +++ b/src/utils/jwt.js @@ -1,9 +1,9 @@ import axiosInstance from '../api/axiosConfig'; import router from '../router'; // 引入 Vue Router 实例 import { justLoggedIn } from './authSessionState'; // Import the flag -import { getTempPrivilege } from '../api/login'; // 导入获取临时权限的函数 const USER_INFO_URL = '/user'; // 获取用户信息的API端点 +const TEMP_PRIVILEGE_URL = '/user/temp_privilege'; // 获取用户临时权限的API端点 /** * 从 sessionStorage 中安全地读取和解析用户信息。 @@ -47,18 +47,24 @@ export const getUserInfo = async () => { const response = await axiosInstance.get(USER_INFO_URL); const user = response.data; + // 进一步获取临时权限信息 try { - // 尝试获取临时权限信息,如果失败不影响用户基本信息的返回 - const tempPrivilegeResponse = await getTempPrivilege(); - if (tempPrivilegeResponse && tempPrivilegeResponse.privilege) { - user.temp_privilege = tempPrivilegeResponse.privilege; + const tempResp = await axiosInstance.get(TEMP_PRIVILEGE_URL); + // 这里假设后端返回格式为 { temp_privilege: 'lv-map;lv-competitor' } + if (tempResp.data) { + if (tempResp.data.temp_privilege !== undefined) { + user.temp_privilege = tempResp.data.temp_privilege; + } else if (typeof tempResp.data.privilege === 'string') { + // 后端返回字段名为 privilege + user.temp_privilege = tempResp.data.privilege; + } } - } catch (error) { - // 如果获取临时权限失败,只记录错误但不影响主流程 - console.warn('Failed to fetch temp privilege:', error); + } catch (e) { + // 若临时权限接口失败,仅记录错误,不影响主流程 + console.error('Failed to fetch temp privilege:', e); } - - // 将获取到的用户信息存入 sessionStorage + + // 将合并后的用户信息存入 sessionStorage sessionStorage.setItem('currentUser', JSON.stringify(user)); return user; } catch (error) { @@ -76,7 +82,7 @@ export const logoutUser = () => { // 不再是 async,因为它不执行异步 // console.log('jwt.js: logoutUser called. Clearing local storage.'); localStorage.removeItem('access_token'); localStorage.removeItem('user_id'); - //sessionStorage.removeItem('currentUser'); // 同时清除sessionStorage中的用户信息 + sessionStorage.removeItem('currentUser'); // 同时清除sessionStorage中的用户信息 // 导航将由调用者(如路由守卫)处理 }; @@ -87,9 +93,9 @@ export const logoutUser = () => { // 不再是 async,因为它不执行异步 */ export const loginSuccess = (accessToken, userId) => { // 清除可能存在的旧数据 - localStorage.removeItem('access_token'); - localStorage.removeItem('user_id'); - sessionStorage.removeItem('currentUser'); + // localStorage.removeItem('access_token'); + // localStorage.removeItem('user_id'); + // sessionStorage.removeItem('currentUser'); // 存储新的认证信息 localStorage.setItem('access_token', accessToken); diff --git a/src/utils/privilege.js b/src/utils/privilege.js index 7c69d2f..73e3d68 100644 --- a/src/utils/privilege.js +++ b/src/utils/privilege.js @@ -3,7 +3,7 @@ export function hasPrivilege(privilege, required) { if (!privilege) return false; // lv-admin 拥有全部权限 if (privilege.includes('lv-admin')) return true; - const privArr = privilege.split(';'); + const privArr = privilege.split(';').map(p => p.trim()).filter(Boolean); if (Array.isArray(required)) { return required.some(r => privArr.includes(r)); } @@ -18,12 +18,12 @@ export function mergePrivileges(permanentPrivilege, tempPrivilege) { // 添加永久权限 if (permanentPrivilege) { - mergedPrivileges.push(...permanentPrivilege.split(';')); + mergedPrivileges.push(...permanentPrivilege.split(';').map(p => p.trim()).filter(Boolean)); } // 添加临时权限(如果存在) if (tempPrivilege) { - mergedPrivileges.push(...tempPrivilege.split(';')); + mergedPrivileges.push(...tempPrivilege.split(';').map(p => p.trim()).filter(Boolean)); } // 去重并返回 diff --git a/src/views/index.vue b/src/views/index.vue index 9bc7d04..8f8d1aa 100644 --- a/src/views/index.vue +++ b/src/views/index.vue @@ -7,6 +7,7 @@ import { requestTempPrivilege } from '@/api/login.js' const PrivilegeRequestDialog = defineAsyncComponent(() => import('@/components/PrivilegeRequestDialog.vue')) const SuccessDialog = defineAsyncComponent(() => import('@/components/SuccessDialog.vue')) +const ErrorDialog = defineAsyncComponent(() => import('@/components/ErrorDialog.vue')) const isLoggedIn = computed(() => { return !!localStorage.getItem('access_token') && !!currentUserData.value @@ -114,10 +115,24 @@ const toggleDropdown = () => { let privilegeCheckTimer = null +// 允许的临时权限列表 +const allowedTempPrivileges = ['lv-mod', 'lv-map', 'lv-competitor', 'lv-user'] + +// 获取当前路由所需的权限数组(如果有) +function getRouteRequiredPrivilege(route) { + const record = route.matched.find(r => r.meta && r.meta.requiredPrivilege) + return record ? record.meta.requiredPrivilege : null +} + onMounted(() => { if (localStorage.getItem('access_token')) { getUserInfo().then(userInfo => { if (userInfo) { + // 检测非法临时权限 + if (hasInvalidTempPrivilege(userInfo)) { + errorDialogMessage.value = '账号异常:检测到非法临时权限,请重新登录或联系管理员。' + errorDialogVisible.value = true + } currentUserData.value = userInfo } else { console.log('Index.vue: Failed to get user info, token might be invalid.') @@ -128,19 +143,30 @@ onMounted(() => { // 添加点击外部关闭下拉菜单的监听器 document.addEventListener('click', handleClickOutside) - // 启动权限检查定时器(每30秒检查一次) + // 启动权限检查定时器(每10秒检查一次) privilegeCheckTimer = setInterval(async () => { if (localStorage.getItem('access_token') && currentUserData.value) { try { const userInfo = await getUserInfo() if (userInfo) { + // 检测非法临时权限 + if (hasInvalidTempPrivilege(userInfo)) { + errorDialogMessage.value = '账号异常:检测到非法临时权限,请重新登录或联系管理员。' + errorDialogVisible.value = true + } // 检查临时权限是否消失 const hadTempPrivilege = currentUserData.value.temp_privilege && currentUserData.value.temp_privilege.trim() !== '' const hasTempPrivilege = userInfo.temp_privilege && userInfo.temp_privilege.trim() !== '' - // 如果之前有临时权限,现在没有了,则重定向到maps页面 + // 如果之前有临时权限,现在没有了,则使用 replace 重定向到 maps,避免浏览器返回到无权限页 if (hadTempPrivilege && !hasTempPrivilege) { - router.push('/maps') + router.replace('/maps') + } + + // 如果当前路由需要特定权限,但用户已不满足,则重定向到 /maps + const requiredPriv = getRouteRequiredPrivilege(router.currentRoute.value) + if (requiredPriv && !hasPrivilegeWithTemp(userInfo, requiredPriv)) { + router.replace('/maps') } currentUserData.value = userInfo @@ -149,7 +175,7 @@ onMounted(() => { console.log('权限检查失败:', error) } } - }, 30 * 1000) // 30秒 + }, 10 * 1000) // 10秒 }) onUnmounted(() => { @@ -232,6 +258,13 @@ function handleNavClick(route, privilegeList) { } router.push(route) } + +// 检测临时权限是否合法 +function hasInvalidTempPrivilege(user) { + if (!user || !user.temp_privilege) return false + const arr = user.temp_privilege.split(';').map(p => p.trim()).filter(Boolean) + return arr.some(p => !allowedTempPrivileges.includes(p)) +}