2025-10-07 13:31:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @description 路由守卫和权限检查
|
|
|
|
|
|
* @author yslg
|
|
|
|
|
|
* @since 2025-10-07
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import type { Router, NavigationGuardNext, RouteLocationNormalized } from 'vue-router';
|
|
|
|
|
|
import type { Store } from 'vuex';
|
|
|
|
|
|
import { AuthState } from '@/store/modules/auth';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 白名单路由 - 无需登录即可访问
|
|
|
|
|
|
*/
|
|
|
|
|
|
const WHITE_LIST = [
|
|
|
|
|
|
'/login',
|
|
|
|
|
|
'/register',
|
|
|
|
|
|
'/forgot-password',
|
2025-10-08 14:11:54 +08:00
|
|
|
|
'/home',
|
2025-10-07 13:31:06 +08:00
|
|
|
|
'/403',
|
2025-10-18 18:19:19 +08:00
|
|
|
|
'/404', // 404页面允许访问(但未登录时不会被路由到这里)
|
2025-10-07 13:31:06 +08:00
|
|
|
|
'/500'
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置路由守卫
|
|
|
|
|
|
* @param router Vue Router实例
|
|
|
|
|
|
* @param store Vuex Store实例
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function setupRouterGuards(router: Router, store: Store<any>) {
|
|
|
|
|
|
|
|
|
|
|
|
// 全局前置守卫
|
|
|
|
|
|
router.beforeEach(async (to, from, next) => {
|
|
|
|
|
|
// 开始页面加载进度条
|
|
|
|
|
|
startProgress();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await handleRouteGuard(to, from, next, store);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('路由守卫执行失败:', error);
|
|
|
|
|
|
// 发生错误时跳转到500页面
|
|
|
|
|
|
next('/500');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 全局后置钩子
|
|
|
|
|
|
|
|
|
|
|
|
router.afterEach((to) => {
|
|
|
|
|
|
// 结束页面加载进度条
|
|
|
|
|
|
finishProgress();
|
|
|
|
|
|
|
|
|
|
|
|
// 设置页面标题
|
|
|
|
|
|
setPageTitle(to.meta?.title as string);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 全局解析守卫(在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后调用)
|
|
|
|
|
|
router.beforeResolve(async (to, from, next) => {
|
|
|
|
|
|
// 这里可以处理一些最终的权限检查或数据预加载
|
|
|
|
|
|
next();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理路由守卫逻辑
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function handleRouteGuard(
|
|
|
|
|
|
to: RouteLocationNormalized,
|
|
|
|
|
|
from: RouteLocationNormalized,
|
|
|
|
|
|
next: NavigationGuardNext,
|
|
|
|
|
|
store: Store<any>
|
|
|
|
|
|
) {
|
|
|
|
|
|
const authState: AuthState = store.state.auth;
|
2025-10-08 14:11:54 +08:00
|
|
|
|
const isAuthenticated = store.getters['auth/isAuthenticated'];
|
2025-10-07 13:31:06 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查是否在白名单中
|
|
|
|
|
|
if (isInWhiteList(to.path)) {
|
|
|
|
|
|
return next();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查用户是否已登录
|
|
|
|
|
|
if (!isAuthenticated) {
|
|
|
|
|
|
// 未登录,重定向到登录页
|
|
|
|
|
|
return next({
|
|
|
|
|
|
path: '/login',
|
|
|
|
|
|
query: {
|
|
|
|
|
|
redirect: to.fullPath // 记录用户想要访问的页面
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 用户已登录,检查是否需要生成动态路由
|
2025-10-08 14:11:54 +08:00
|
|
|
|
// 注意:通常情况下路由应该在 main.ts 初始化时就已经生成
|
|
|
|
|
|
// 这里主要处理登录后首次生成路由的情况
|
|
|
|
|
|
if (!authState.routesLoaded && authState.menus && authState.menus.length > 0) {
|
2025-10-07 13:31:06 +08:00
|
|
|
|
try {
|
2025-10-08 14:11:54 +08:00
|
|
|
|
console.log('[路由守卫] 路由未加载,开始生成动态路由');
|
2025-10-07 13:31:06 +08:00
|
|
|
|
// 生成动态路由
|
|
|
|
|
|
await store.dispatch('auth/generateRoutes');
|
|
|
|
|
|
|
2025-10-08 14:11:54 +08:00
|
|
|
|
console.log('[路由守卫] 动态路由生成成功,重新导航');
|
2025-10-07 13:31:06 +08:00
|
|
|
|
// 重新导航到目标路由
|
|
|
|
|
|
return next({ ...to, replace: true });
|
|
|
|
|
|
} catch (error) {
|
2025-10-08 14:11:54 +08:00
|
|
|
|
console.error('[路由守卫] 生成动态路由失败:', error);
|
2025-10-07 13:31:06 +08:00
|
|
|
|
// 清除认证信息并跳转到登录页
|
|
|
|
|
|
store.commit('auth/CLEAR_AUTH');
|
|
|
|
|
|
return next('/login');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查页面权限
|
|
|
|
|
|
const hasPermission = await checkPagePermission(to, store);
|
|
|
|
|
|
if (!hasPermission) {
|
|
|
|
|
|
// 无权限访问,跳转到403页面
|
|
|
|
|
|
return next('/403');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 所有检查通过,继续导航
|
|
|
|
|
|
next();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查路径是否在白名单中
|
|
|
|
|
|
*/
|
|
|
|
|
|
function isInWhiteList(path: string): boolean {
|
|
|
|
|
|
return WHITE_LIST.some(whitePath => {
|
|
|
|
|
|
if (whitePath.endsWith('*')) {
|
|
|
|
|
|
// 支持通配符匹配
|
|
|
|
|
|
const prefix = whitePath.slice(0, -1);
|
|
|
|
|
|
return path.startsWith(prefix);
|
|
|
|
|
|
}
|
|
|
|
|
|
return path === whitePath;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查页面权限
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function checkPagePermission(
|
|
|
|
|
|
route: RouteLocationNormalized,
|
|
|
|
|
|
store: Store<any>
|
|
|
|
|
|
): Promise<boolean> {
|
|
|
|
|
|
// 如果路由元信息中没有要求权限,则允许访问
|
|
|
|
|
|
if (route.meta?.requiresAuth === false) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查路由是否需要特定权限
|
|
|
|
|
|
const requiredPermissions = route.meta?.permissions as string[] | undefined;
|
|
|
|
|
|
|
|
|
|
|
|
if (!requiredPermissions || requiredPermissions.length === 0) {
|
|
|
|
|
|
// 无特定权限要求,但需要登录,已经在前面检查过登录状态
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查用户是否有所需权限
|
|
|
|
|
|
const hasPermission = store.getters['auth/hasAnyPermission'];
|
|
|
|
|
|
return hasPermission(requiredPermissions);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置页面标题
|
|
|
|
|
|
*/
|
|
|
|
|
|
function setPageTitle(title?: string) {
|
|
|
|
|
|
const appTitle = '校园新闻管理系统';
|
|
|
|
|
|
document.title = title ? `${title} - ${appTitle}` : appTitle;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 开始进度条(可以集成 NProgress 或其他进度条库)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function startProgress() {
|
|
|
|
|
|
// TODO: 集成进度条库,如 NProgress
|
|
|
|
|
|
// NProgress.start();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 完成进度条
|
|
|
|
|
|
*/
|
|
|
|
|
|
function finishProgress() {
|
|
|
|
|
|
// TODO: 集成进度条库,如 NProgress
|
|
|
|
|
|
// NProgress.done();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Token自动刷新中间件
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function setupTokenRefresh(store: Store<any>) {
|
|
|
|
|
|
// 设置定时器自动刷新Token
|
|
|
|
|
|
setInterval(async () => {
|
|
|
|
|
|
const authState: AuthState = store.state.auth;
|
|
|
|
|
|
|
|
|
|
|
|
if (!authState.token || !authState.loginDomain) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查Token是否快要过期(例如:提前5分钟刷新)
|
|
|
|
|
|
const tokenExpireTime = authState.loginDomain.tokenExpireTime;
|
|
|
|
|
|
if (tokenExpireTime) {
|
|
|
|
|
|
const expireTime = new Date(tokenExpireTime).getTime();
|
|
|
|
|
|
const currentTime = Date.now();
|
|
|
|
|
|
const fiveMinutes = 5 * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
|
|
if (expireTime - currentTime <= fiveMinutes) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await store.dispatch('auth/refreshToken');
|
|
|
|
|
|
console.log('Token自动刷新成功');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Token自动刷新失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 60 * 1000); // 每分钟检查一次
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 权限检查工具函数
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class PermissionChecker {
|
|
|
|
|
|
private store: Store<any>;
|
|
|
|
|
|
|
|
|
|
|
|
constructor(store: Store<any>) {
|
|
|
|
|
|
this.store = store;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查是否有指定权限
|
|
|
|
|
|
*/
|
|
|
|
|
|
hasPermission(permissionCode: string): boolean {
|
|
|
|
|
|
return this.store.getters['auth/hasPermission'](permissionCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查是否有任意一个权限
|
|
|
|
|
|
*/
|
|
|
|
|
|
hasAnyPermission(permissionCodes: string[]): boolean {
|
|
|
|
|
|
return this.store.getters['auth/hasAnyPermission'](permissionCodes);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查是否有所有权限
|
|
|
|
|
|
*/
|
|
|
|
|
|
hasAllPermissions(permissionCodes: string[]): boolean {
|
|
|
|
|
|
return this.store.getters['auth/hasAllPermissions'](permissionCodes);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查是否有指定角色
|
|
|
|
|
|
*/
|
|
|
|
|
|
hasRole(roleCode: string): boolean {
|
|
|
|
|
|
const userRoles = this.store.getters['auth/userRoles'];
|
|
|
|
|
|
return userRoles.some((role: any) => role.code === roleCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查是否有任意一个角色
|
|
|
|
|
|
*/
|
|
|
|
|
|
hasAnyRole(roleCodes: string[]): boolean {
|
|
|
|
|
|
return roleCodes.some(code => this.hasRole(code));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|