Vue 3 路由守卫与权限控制完全指南

924 阅读4分钟

Vue 3 路由守卫与权限控制完全指南

一、路由守卫架构设计

1. 路由守卫类型

// types/router.ts
export interface RouteMeta {
  title?: string
  auth?: boolean
  roles?: string[]
  permissions?: string[]
  cache?: boolean
}

export interface UserState {
  token?: string
  roles: string[]
  permissions: string[]
}

export type NavigationGuard = (to: RouteLocation, from: RouteLocation, next: NavigationGuardNext) => Promise<void> | void

2. 基础路由配置

// router/routes.ts
import { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: {
      title: '登录',
      auth: false
    }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: {
      title: '控制台',
      auth: true,
      roles: ['admin', 'user'],
      permissions: ['dashboard:view']
    }
  }
]

export default routes

二、全局路由守卫实现

1. 权限控制器

// utils/permission.ts
export class PermissionController {
  private user: UserState
  
  constructor(userState: UserState) {
    this.user = userState
  }
  
  // 检查角色权限
  hasRole(roles: string[]): boolean {
    if (!roles || roles.length === 0) return true
    return this.user.roles.some(role => roles.includes(role))
  }
  
  // 检查功能权限
  hasPermission(permissions: string[]): boolean {
    if (!permissions || permissions.length === 0) return true
    return this.user.permissions.some(
      permission => permissions.includes(permission)
    )
  }
  
  // 检查路由访问权限
  canAccess(routeMeta: RouteMeta): boolean {
    if (!routeMeta.auth) return true
    
    // 检查是否登录
    if (!this.user.token) return false
    
    // 检查角色和权限
    return this.hasRole(routeMeta.roles || []) &&
           this.hasPermission(routeMeta.permissions || [])
  }
}

2. 全局守卫配置

// router/guards.ts
import { Router, RouteLocationNormalized } from 'vue-router'
import { PermissionController } from '@/utils/permission'
import { useUserStore } from '@/stores/user'

export function setupGuards(router: Router) {
  // 标题守卫
  const titleGuard = (to: RouteLocationNormalized) => {
    const title = to.meta.title
    if (title) {
      document.title = `${title} - 系统名称`
    }
  }
  
  // 权限守卫
  const permissionGuard: NavigationGuard = async (to, from, next) => {
    const userStore = useUserStore()
    const permissionCtrl = new PermissionController(userStore.state)
    
    // 检查路由是否需要认证
    if (!to.meta.auth) {
      next()
      return
    }
    
    // 检查用户是否登录
    if (!userStore.isLoggedIn) {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
      return
    }
    
    // 检查访问权限
    if (!permissionCtrl.canAccess(to.meta)) {
      next('/403')
      return
    }
    
    next()
  }
  
  // 进度条守卫
  const loadingGuard = () => {
    NProgress.start()
    return () => {
      NProgress.done()
    }
  }
  
  // 注册全局守卫
  router.beforeEach(async (to, from, next) => {
    const cleanup = loadingGuard()
    
    try {
      await permissionGuard(to, from, next)
      titleGuard(to)
    } finally {
      cleanup()
    }
  })
}

三、动态路由控制

1. 动态路由生成

// router/dynamic.ts
export class DynamicRouteBuilder {
  private router: Router
  private permissionCtrl: PermissionController
  
  constructor(router: Router, permissionCtrl: PermissionController) {
    this.router = router
    this.permissionCtrl = permissionCtrl
  }
  
  // 过滤路由配置
  filterRoutes(routes: RouteRecordRaw[]): RouteRecordRaw[] {
    return routes.filter(route => {
      // 检查当前路由权限
      if (!this.permissionCtrl.canAccess(route.meta || {})) {
        return false
      }
      
      // 递归处理子路由
      if (route.children) {
        route.children = this.filterRoutes(route.children)
      }
      
      return true
    })
  }
  
  // 添加动态路由
  async addRoutes(routes: RouteRecordRaw[]) {
    const accessibleRoutes = this.filterRoutes(routes)
    
    for (const route of accessibleRoutes) {
      this.router.addRoute(route)
    }
    
    return accessibleRoutes
  }
}

2. 路由初始化

// router/index.ts
export async function initRouter() {
  const router = createRouter({
    history: createWebHistory(),
    routes: constantRoutes // 基础路由
  })
  
  const userStore = useUserStore()
  
  // 初始化权限控制器
  const permissionCtrl = new PermissionController(userStore.state)
  
  // 初始化动态路由构建器
  const routeBuilder = new DynamicRouteBuilder(router, permissionCtrl)
  
  // 设置路由守卫
  setupGuards(router)
  
  // 加载动态路由
  if (userStore.isLoggedIn) {
    // 可以从后端获取路由配置
    const asyncRoutes = await fetchUserRoutes()
    await routeBuilder.addRoutes(asyncRoutes)
  }
  
  return router
}

四、组件级权限控制

1. 权限指令

// directives/permission.ts
import { DirectiveBinding } from 'vue'
import { PermissionController } from '@/utils/permission'

export const permission = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const { value } = binding
    const userStore = useUserStore()
    const permissionCtrl = new PermissionController(userStore.state)
    
    if (!permissionCtrl.hasPermission(value)) {
      el.parentNode?.removeChild(el)
    }
  }
}

// 使用示例
<button v-permission="['user:add']">添加用户</button>

2. 权限组件

<!-- components/Permission.vue -->
<template>
  <template v-if="hasPermission">
    <slot />
  </template>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'

const props = defineProps<{
  roles?: string[]
  permissions?: string[]
}>()

const userStore = useUserStore()
const permissionCtrl = new PermissionController(userStore.state)

const hasPermission = computed(() => {
  return permissionCtrl.hasRole(props.roles || []) &&
         permissionCtrl.hasPermission(props.permissions || [])
})
</script>

<!-- 使用示例 -->
<Permission :roles="['admin']" :permissions="['user:edit']">
  <button>编辑用户</button>
</Permission>

五、菜单权限控制

1. 菜单配置

// config/menu.ts
export interface MenuItem {
  key: string
  title: string
  icon?: string
  path?: string
  children?: MenuItem[]
  roles?: string[]
  permissions?: string[]
}

export const menuConfig: MenuItem[] = [
  {
    key: 'dashboard',
    title: '控制台',
    icon: 'dashboard',
    path: '/dashboard',
    permissions: ['dashboard:view']
  },
  {
    key: 'user',
    title: '用户管理',
    icon: 'user',
    roles: ['admin'],
    children: [
      {
        key: 'user-list',
        title: '用户列表',
        path: '/user/list',
        permissions: ['user:list']
      }
    ]
  }
]

2. 菜单生成器

// components/Menu/generator.ts
export class MenuGenerator {
  private permissionCtrl: PermissionController
  
  constructor(permissionCtrl: PermissionController) {
    this.permissionCtrl = permissionCtrl
  }
  
  // 过滤菜单项
  filterMenuItems(items: MenuItem[]): MenuItem[] {
    return items.filter(item => {
      // 检查权限
      if (!this.permissionCtrl.hasRole(item.roles || []) ||
          !this.permissionCtrl.hasPermission(item.permissions || [])) {
        return false
      }
      
      // 处理子菜单
      if (item.children) {
        item.children = this.filterMenuItems(item.children)
        return item.children.length > 0
      }
      
      return true
    })
  }
  
  // 生成菜单树
  generateMenu(config: MenuItem[]): MenuItem[] {
    return this.filterMenuItems(config)
  }
}

六、缓存控制

1. 路由缓存

// router/cache.ts
export class RouteCache {
  private cache = new Set<string>()
  
  // 添加缓存路由
  add(name: string) {
    this.cache.add(name)
  }
  
  // 移除缓存路由
  remove(name: string) {
    this.cache.delete(name)
  }
  
  // 获取所有需要缓存的路由
  getList(): string[] {
    return Array.from(this.cache)
  }
  
  // 清空缓存
  clear() {
    this.cache.clear()
  }
}

// 在路由守卫中使用
const cacheGuard: NavigationGuard = (to, from, next) => {
  const routeCache = new RouteCache()
  
  if (to.meta.cache) {
    routeCache.add(to.name as string)
  }
  
  next()
}

2. 组件缓存控制

<!-- layouts/MainLayout.vue -->
<template>
  <div class="layout">
    <keep-alive :include="cachedRoutes">
      <router-view />
    </keep-alive>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const routeCache = new RouteCache()

const cachedRoutes = computed(() => routeCache.getList())
</script>

七、权限更新处理

1. 权限刷新

// stores/permission.ts
export const usePermissionStore = defineStore('permission', {
  state: () => ({
    routes: [] as RouteRecordRaw[],
    menus: [] as MenuItem[]
  }),
  
  actions: {
    async refreshPermissions() {
      // 获取最新权限
      const newPermissions = await fetchUserPermissions()
      
      // 更新用户状态
      const userStore = useUserStore()
      userStore.updatePermissions(newPermissions)
      
      // 重新生成路由
      const router = useRouter()
      const permissionCtrl = new PermissionController(userStore.state)
      const routeBuilder = new DynamicRouteBuilder(router, permissionCtrl)
      
      // 清空现有路由
      this.routes.forEach(route => {
        router.removeRoute(route.name as string)
      })
      
      // 添加新路由
      const asyncRoutes = await fetchUserRoutes()
      this.routes = await routeBuilder.addRoutes(asyncRoutes)
      
      // 重新生成菜单
      const menuGenerator = new MenuGenerator(permissionCtrl)
      this.menus = menuGenerator.generateMenu(menuConfig)
    }
  }
})

八、最佳实践建议

  1. 性能优化

    • 路由组件懒加载
    • 合理使用路由缓存
    • 避免不必要的权限检查
  2. 安全考虑

    • 前后端权限双重验证
    • 定期刷新权限状态
    • Token 安全存储
  3. 用户体验

    • 权限不足时的友好提示
    • 路由切换进度条
    • 保持导航状态
  4. 代码质量

    • 完善的类型定义
    • 统一的错误处理
    • 模块化设计

总结

本指南涵盖了:

  1. 全局路由守卫实现
  2. 动态路由控制
  3. 组件级权限控制
  4. 菜单权限管理
  5. 缓存控制策略

核心要点:

  • 职责分离
  • 类型安全
  • 灵活配置
  • 性能优化

参考资源