Vue3路由权限动态管理方案
一、方案概述
我的项目采用 基于角色的访问控制(RBAC) 实现路由权限动态管理,核心思路是:根据用户角色动态加载可访问的路由表,实现细粒度的权限控制。
二、核心架构
2.1 整体架构图
┌─────────────────────────────────────────────────────────────────┐
│ 前端路由权限系统 │
├─────────────────────────────────────────────────────────────────┤
│ 用户登录 → 获取用户信息 → 动态加载路由 → 路由守卫校验 → 页面渲染 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐│
│ │ 登录页面 │ → │ UserStore │ → │ Permission │ → │ Router ││
│ │ │ │ 获取角色 │ │ 动态路由生成 │ │ 路由守卫 ││
│ └──────────┘ └──────────┘ └──────────────┘ └──────────┘│
│ ↓ ↓ │
│ ┌───────────────────────────────────────────────────────────────┐│
│ │ asyncRoutes.ts ││
│ │ 定义所有动态路由,通过 meta.roles 指定可访问角色 ││
│ └───────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
2.2 关键文件职责
| 文件路径 | 职责 |
|---|---|
src/router/index.ts |
路由配置入口,集成路由守卫 |
src/router/asyncRoutes.ts |
动态路由定义,包含角色权限配置 |
src/stores/permission.ts |
权限状态管理,负责路由生成与重置 |
src/utils/permission.ts |
权限工具函数,路由守卫逻辑 |
三、实现步骤
3.1 步骤一:定义动态路由
在 asyncRoutes.ts 中定义所有需要权限控制的路由:
typescript
// src/router/asyncRoutes.ts
export const asyncRoutes: RouteRecordRaw[] = [
{
path: '/admin',
name: 'admin',
component: () => import('@/layout/admin/AdminLayout.vue'),
meta: {
title: '管理后台',
roles: ['管理员'] // 指定可访问角色
},
children: [
{
path: 'shop',
name: 'shopManage',
component: () => import('@/views/admin/components/shop/shopProduct.vue'),
meta: {
title: '商铺管理',
roles: ['管理员']
}
}
]
},
{
path: '/foster-manage',
name: 'fosterManage',
component: () => import('@/views/fosterManage/foster-manage.vue'),
meta: {
title: '寄养管理',
roles: ['商家', '管理员'] // 多个角色可访问
}
}
]
3.2 步骤二:权限状态管理
在 permission.ts store 中管理路由加载状态:
typescript
// src/stores/permission.ts
export const usePermissionStore = defineStore('permission', () => {
const isRoutesLoaded = ref(false)
const routes = ref<RouteRecordRaw[]>([])
/**
* 动态生成路由
* @param roles 用户角色列表
*/
async function generateRoutes(roles: string[]) {
// 根据角色筛选可访问路由
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
routes.value = accessedRoutes
// 动态添加到路由表
accessedRoutes.forEach(route => {
router.addRoute(route)
})
isRoutesLoaded.value = true
return accessedRoutes
}
/**
* 重置路由
*/
function resetRouter() {
// 移除所有动态添加的路由
routes.value.forEach(route => {
const { name } = route
if (name) {
router.removeRoute(name)
}
})
routes.value = []
isRoutesLoaded.value = false
}
return {
isRoutesLoaded,
routes,
generateRoutes,
resetRouter
}
})
3.3 步骤三:路由守卫校验
在 permission.ts 工具函数中实现路由守卫逻辑:
typescript
// src/utils/permission.ts
export function setupPermissionGuard(router: Router) {
router.beforeEach(async (to, from, next) => {
// 白名单路由直接放行
if (whiteList.includes(to.path)) {
return next()
}
// 获取用户角色
const userStore = useUserStore(store)
const permissionStore = usePermissionStore(store)
if (!userStore.isLoggedIn()) {
// 未登录,跳转到登录页
return next(`/login?redirect=${to.path}`)
}
// 路由已加载,检查权限
if (permissionStore.isRoutesLoaded) {
// 检查当前路由是否需要权限
if (to.meta?.roles && !to.meta.roles.some(role => userStore.userInfo.roles?.includes(role))) {
// 无权限,跳转到403页面
return next('/403')
}
return next()
}
// 动态加载路由
try {
await permissionStore.generateRoutes(userStore.userInfo.roles || [])
// 重新导航,确保新路由生效
next({ ...to, replace: true })
} catch (error) {
console.error('路由加载失败:', error)
next('/login')
}
})
}
3.4 步骤四:登录时触发路由加载
在登录成功后,调用路由生成方法:
typescript
// src/views/enterPet/login-pet.vue
const handleLogin = async (formData: LoginForm) => {
const response = await loginByAccount({ ... })
if (response.code === 0) {
const { token, userInfo } = response.data
// 更新用户状态
userStore.setToken(token)
userStore.setUserInfo(userInfo)
// 动态生成路由
await permissionStore.generateRoutes(userInfo.roles)
// 跳转到目标页面
router.push('/foster-care')
}
}
四、核心难点
4.1 路由重复加载问题
问题描述:多次触发路由守卫时,动态路由可能被重复添加。
解决方案:
typescript
// 通过 isRoutesLoaded 标志位避免重复加载
if (permissionStore.isRoutesLoaded) {
// 路由已加载,直接检查权限
return checkPermissionAndNext(to, next)
}
4.2 路由导航状态问题
问题描述:动态路由添加后,当前导航可能已处于失败状态。
解决方案:
typescript
// 使用 replace: true 重新导航,避免历史记录重复
next({ ...to, replace: true })
4.3 角色权限匹配逻辑
问题描述:如何判断用户是否有权限访问某个路由。
解决方案:
typescript
// 路由角色列表与用户角色列表的交集判断
function hasPermission(routeRoles: string[], userRoles: string[]) {
if (!routeRoles || routeRoles.length === 0) {
return true // 无角色限制的路由默认可访问
}
return userRoles.some(role => routeRoles.includes(role))
}
4.4 路由重置问题
问题描述:用户退出登录后,动态添加的路由需要清理。
解决方案:
typescript
function resetRouter() {
routes.value.forEach(route => {
const { name } = route
if (name) {
router.removeRoute(name) // 移除路由
}
})
isRoutesLoaded.value = false
}
五、关键代码解析
5.1 路由筛选函数
typescript
// src/stores/permission.ts
function filterAsyncRoutes(routes: RouteRecordRaw[], roles: string[]): RouteRecordRaw[] {
const res: RouteRecordRaw[] = []
routes.forEach(route => {
const temp = { ...route }
// 检查当前路由是否有权限
if (hasPermission(temp.meta?.roles, roles)) {
// 递归处理子路由
if (temp.children) {
temp.children = filterAsyncRoutes(temp.children, roles)
}
res.push(temp)
}
})
return res
}
解析:递归遍历路由树,根据用户角色筛选可访问的路由,确保子路由也遵循相同的权限规则。
5.2 路由守卫完整逻辑
typescript
// src/utils/permission.ts
router.beforeEach(async (to, from, next) => {
// 1. 白名单校验
if (whiteList.includes(to.path)) {
return next()
}
// 2. 登录状态校验
if (!userStore.isLoggedIn()) {
return next(`/login?redirect=${to.path}`)
}
// 3. 路由加载状态校验
if (!permissionStore.isRoutesLoaded) {
await permissionStore.generateRoutes(userStore.userInfo.roles || [])
return next({ ...to, replace: true })
}
// 4. 权限校验
if (to.meta?.roles) {
if (!hasPermission(to.meta.roles, userStore.userInfo.roles || [])) {
return next('/403')
}
}
next()
})
解析:路由守卫按照"白名单 → 登录状态 → 路由加载 → 权限校验"的顺序进行层层校验,确保安全性。
六、权限控制流程总结
| 阶段 | 操作 | 文件 |
|---|---|---|
| 1. 定义路由 | 在 asyncRoutes.ts 中定义动态路由及角色权限 | router/asyncRoutes.ts |
| 2. 登录认证 | 获取用户角色信息,保存到 Pinia store | stores/user.ts |
| 3. 动态加载 | 根据角色筛选路由,添加到路由表 | stores/permission.ts |
| 4. 路由守卫 | 校验登录状态、权限,控制导航 | utils/permission.ts |
| 5. 页面渲染 | 根据路由配置渲染对应页面 | views/**/*.vue |
| 6. 退出清理 | 重置路由表,清除权限状态 | stores/permission.ts |
七、安全性保障
- 前端校验:路由守卫在前端层面控制页面访问,但不能完全依赖
- 后端校验:所有接口必须独立进行权限校验,防止绕过前端直接访问
- Token 验证:每次请求携带 Token,后端验证有效性
- 路由重置:退出登录时清理动态路由,防止信息泄露