Skip to content

Vue Router 导航守卫与权限控制

导航守卫是Vue Router提供的路由跳转过程中的钩子函数,可以用来控制路由的跳转、取消或重定向。

🛡️ 导航守卫类型

mermaid
graph TB
    A[导航守卫] --> B[全局守卫]
    A --> C[路由独享守卫]
    A --> D[组件内守卫]
    
    B --> B1[beforeEach]
    B --> B2[beforeResolve]
    B --> B3[afterEach]
    
    C --> C1[beforeEnter]
    
    D --> D1[beforeRouteEnter]
    D --> D2[beforeRouteUpdate]
    D --> D3[beforeRouteLeave]
    
    subgraph "执行顺序"
        E[导航被触发] --> F[失活组件守卫]
        F --> G[全局beforeEach]
        G --> H[路由beforeEnter]
        H --> I[组件beforeRouteEnter]
        I --> J[全局beforeResolve]
        J --> K[导航确认]
        K --> L[全局afterEach]
        L --> M[DOM更新]
        M --> N[beforeRouteEnter回调]
    end

🔧 导航守卫详解

1. 全局前置守卫

javascript
const router = createRouter({ ... })

router.beforeEach((to, from, next) => {
  // to: 即将要进入的目标路由对象
  // from: 当前导航正要离开的路由
  // next: 一定要调用该方法来resolve这个钩子
  
  console.log('全局前置守卫')
  console.log('to:', to.path)
  console.log('from:', from.path)
  
  // 必须调用next()
  next()
})

// Vue Router 4 的新写法(推荐)
router.beforeEach(async (to, from) => {
  // 返回 false 以取消导航
  if (to.path === '/forbidden') {
    return false
  }
  
  // 返回一个路由地址来重定向
  if (to.path === '/old-path') {
    return '/new-path'
  }
  
  // 什么都不返回或返回 true 则继续导航
})

2. 全局解析守卫

javascript
router.beforeResolve(async (to, from) => {
  console.log('全局解析守卫')
  
  // 在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后调用
  // 这是获取数据或执行任何其他操作的理想位置
  
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return '/login'
  }
})

3. 全局后置钩子

javascript
router.afterEach((to, from, failure) => {
  console.log('全局后置钩子')
  
  // 这些钩子不会接受 next 函数也不会改变导航本身
  // 对于分析、更改页面标题、声明页面等辅助功能很有用
  
  // 更新页面标题
  document.title = to.meta.title || 'Default Title'
  
  // 发送页面访问统计
  if (!failure) {
    sendAnalytics(to.path)
  }
})

4. 路由独享守卫

javascript
const routes = [
  {
    path: '/admin',
    component: AdminPanel,
    beforeEnter: (to, from) => {
      console.log('路由独享守卫')
      
      // 只有管理员可以访问
      if (!hasAdminRole()) {
        return '/unauthorized'
      }
    }
  },
  {
    path: '/users/:id',
    component: UserProfile,
    beforeEnter: [
      // 可以传入多个守卫函数
      checkAuth,
      checkPermission,
      loadUserData
    ]
  }
]

function checkAuth(to, from) {
  if (!isAuthenticated()) {
    return '/login'
  }
}

function checkPermission(to, from) {
  const userId = to.params.id
  if (!canAccessUser(userId)) {
    return '/forbidden'
  }
}

async function loadUserData(to, from) {
  const userId = to.params.id
  try {
    const user = await fetchUser(userId)
    to.meta.user = user
  } catch (error) {
    return '/user-not-found'
  }
}

5. 组件内守卫

javascript
export default {
  name: 'UserProfile',
  
  // 在渲染该组件的对应路由被确认前调用
  // 不能获取组件实例 this,因为当守卫执行前,组件实例还没被创建
  beforeRouteEnter(to, from, next) {
    console.log('beforeRouteEnter')
    
    // 可以通过传一个回调给 next 来访问组件实例
    next(vm => {
      // 通过 vm 访问组件实例
      vm.loadUserData(to.params.id)
    })
  },
  
  // 在当前路由改变,但是该组件被复用时调用
  // 举例来说,对于一个带有动态参数的路径 /users/:id,在 /users/1 和 /users/2 之间跳转的时候
  // 由于会渲染同样的 UserProfile 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
  beforeRouteUpdate(to, from) {
    console.log('beforeRouteUpdate')
    
    // 可以访问组件实例 this
    this.loadUserData(to.params.id)
  },
  
  // 导航离开该组件的对应路由时调用
  beforeRouteLeave(to, from) {
    console.log('beforeRouteLeave')
    
    // 可以访问组件实例 this
    if (this.hasUnsavedChanges) {
      const answer = window.confirm('你有未保存的更改,确定要离开吗?')
      if (!answer) return false
    }
  },
  
  methods: {
    async loadUserData(userId) {
      this.loading = true
      try {
        this.user = await fetchUser(userId)
      } catch (error) {
        this.error = error.message
      } finally {
        this.loading = false
      }
    }
  }
}

🔐 权限控制实现

1. 基于角色的权限控制(RBAC)

javascript
// 权限管理模块
class PermissionManager {
  constructor() {
    this.user = null
    this.permissions = new Set()
    this.roles = new Set()
  }
  
  setUser(user) {
    this.user = user
    this.roles = new Set(user.roles || [])
    this.permissions = new Set(user.permissions || [])
  }
  
  hasRole(role) {
    return this.roles.has(role)
  }
  
  hasPermission(permission) {
    return this.permissions.has(permission)
  }
  
  hasAnyRole(roles) {
    return roles.some(role => this.hasRole(role))
  }
  
  hasAllRoles(roles) {
    return roles.every(role => this.hasRole(role))
  }
  
  hasAnyPermission(permissions) {
    return permissions.some(permission => this.hasPermission(permission))
  }
  
  canAccess(route) {
    const { meta } = route
    
    // 公开路由
    if (!meta.requiresAuth) {
      return true
    }
    
    // 需要登录
    if (!this.user) {
      return false
    }
    
    // 检查角色
    if (meta.roles && !this.hasAnyRole(meta.roles)) {
      return false
    }
    
    // 检查权限
    if (meta.permissions && !this.hasAnyPermission(meta.permissions)) {
      return false
    }
    
    return true
  }
}

const permissionManager = new PermissionManager()

// 路由配置
const routes = [
  {
    path: '/dashboard',
    component: Dashboard,
    meta: {
      requiresAuth: true,
      title: '仪表板'
    }
  },
  {
    path: '/admin',
    component: AdminPanel,
    meta: {
      requiresAuth: true,
      roles: ['admin', 'super_admin'],
      title: '管理面板'
    }
  },
  {
    path: '/users',
    component: UserManagement,
    meta: {
      requiresAuth: true,
      permissions: ['user.read', 'user.write'],
      title: '用户管理'
    }
  }
]

// 全局守卫
router.beforeEach(async (to, from) => {
  // 检查权限
  if (!permissionManager.canAccess(to)) {
    if (!permissionManager.user) {
      // 未登录,跳转到登录页
      return {
        path: '/login',
        query: { redirect: to.fullPath }
      }
    } else {
      // 已登录但权限不足
      return '/forbidden'
    }
  }
  
  // 设置页面标题
  if (to.meta.title) {
    document.title = to.meta.title
  }
})

2. 动态路由权限

javascript
// 动态路由生成
class DynamicRouteManager {
  constructor(router, permissionManager) {
    this.router = router
    this.permissionManager = permissionManager
    this.asyncRoutes = []
  }
  
  // 异步路由配置
  getAsyncRoutes() {
    return [
      {
        path: '/system',
        component: Layout,
        meta: {
          title: '系统管理',
          roles: ['admin']
        },
        children: [
          {
            path: 'users',
            component: () => import('@/views/system/Users.vue'),
            meta: {
              title: '用户管理',
              permissions: ['system.user.read']
            }
          },
          {
            path: 'roles',
            component: () => import('@/views/system/Roles.vue'),
            meta: {
              title: '角色管理',
              permissions: ['system.role.read']
            }
          }
        ]
      },
      {
        path: '/business',
        component: Layout,
        meta: {
          title: '业务管理',
          roles: ['business_admin', 'operator']
        },
        children: [
          {
            path: 'orders',
            component: () => import('@/views/business/Orders.vue'),
            meta: {
              title: '订单管理',
              permissions: ['business.order.read']
            }
          }
        ]
      }
    ]
  }
  
  // 过滤有权限的路由
  filterAsyncRoutes(routes) {
    const accessibleRoutes = []
    
    routes.forEach(route => {
      const tmp = { ...route }
      
      if (this.hasPermission(tmp)) {
        if (tmp.children) {
          tmp.children = this.filterAsyncRoutes(tmp.children)
        }
        accessibleRoutes.push(tmp)
      }
    })
    
    return accessibleRoutes
  }
  
  hasPermission(route) {
    const { meta } = route
    
    if (meta.roles && !this.permissionManager.hasAnyRole(meta.roles)) {
      return false
    }
    
    if (meta.permissions && !this.permissionManager.hasAnyPermission(meta.permissions)) {
      return false
    }
    
    return true
  }
  
  // 生成可访问的路由
  async generateRoutes() {
    const asyncRoutes = this.getAsyncRoutes()
    const accessibleRoutes = this.filterAsyncRoutes(asyncRoutes)
    
    // 添加到路由器
    accessibleRoutes.forEach(route => {
      this.router.addRoute(route)
    })
    
    this.asyncRoutes = accessibleRoutes
    return accessibleRoutes
  }
  
  // 重置路由
  resetRoutes() {
    this.asyncRoutes.forEach(route => {
      if (route.name) {
        this.router.removeRoute(route.name)
      }
    })
    this.asyncRoutes = []
  }
}

// 使用示例
const dynamicRouteManager = new DynamicRouteManager(router, permissionManager)

// 登录后生成路由
async function handleLogin(credentials) {
  try {
    const response = await login(credentials)
    const { user, token } = response.data
    
    // 设置用户信息
    permissionManager.setUser(user)
    
    // 生成动态路由
    await dynamicRouteManager.generateRoutes()
    
    // 跳转到首页或重定向页面
    const redirect = route.query.redirect || '/'
    router.push(redirect)
    
  } catch (error) {
    console.error('登录失败:', error)
  }
}

// 登出时重置路由
function handleLogout() {
  // 重置路由
  dynamicRouteManager.resetRoutes()
  
  // 清除用户信息
  permissionManager.setUser(null)
  
  // 跳转到登录页
  router.push('/login')
}

3. 页面级权限控制

javascript
// 权限指令
const permissionDirective = {
  mounted(el, binding) {
    const { value } = binding
    const hasPermission = checkPermission(value)
    
    if (!hasPermission) {
      el.parentNode && el.parentNode.removeChild(el)
    }
  },
  
  updated(el, binding) {
    const { value, oldValue } = binding
    
    if (value !== oldValue) {
      const hasPermission = checkPermission(value)
      
      if (!hasPermission) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    }
  }
}

function checkPermission(value) {
  if (!value) return true
  
  if (Array.isArray(value)) {
    return permissionManager.hasAnyPermission(value)
  } else if (typeof value === 'string') {
    return permissionManager.hasPermission(value)
  } else if (value.roles) {
    return permissionManager.hasAnyRole(value.roles)
  } else if (value.permissions) {
    return permissionManager.hasAnyPermission(value.permissions)
  }
  
  return false
}

// 注册指令
app.directive('permission', permissionDirective)

// 使用示例
<template>
  <div>
    <!-- 只有管理员可以看到 -->
    <button v-permission="['admin']">删除用户</button>
    
    <!-- 有特定权限才能看到 -->
    <button v-permission="'user.delete'">删除</button>
    
    <!-- 复杂权限控制 -->
    <div v-permission="{ roles: ['admin'], permissions: ['system.config'] }">
      系统配置
    </div>
  </div>
</template>

4. 组件级权限控制

javascript
// 权限混入
const permissionMixin = {
  methods: {
    hasRole(role) {
      return permissionManager.hasRole(role)
    },
    
    hasPermission(permission) {
      return permissionManager.hasPermission(permission)
    },
    
    hasAnyRole(roles) {
      return permissionManager.hasAnyRole(roles)
    },
    
    hasAnyPermission(permissions) {
      return permissionManager.hasAnyPermission(permissions)
    },
    
    checkAccess(config) {
      if (config.roles && !this.hasAnyRole(config.roles)) {
        return false
      }
      
      if (config.permissions && !this.hasAnyPermission(config.permissions)) {
        return false
      }
      
      return true
    }
  }
}

// 权限组件
const PermissionWrapper = {
  name: 'PermissionWrapper',
  props: {
    roles: Array,
    permissions: Array,
    fallback: {
      type: [String, Object],
      default: null
    }
  },
  
  setup(props, { slots }) {
    const hasAccess = computed(() => {
      if (props.roles && !permissionManager.hasAnyRole(props.roles)) {
        return false
      }
      
      if (props.permissions && !permissionManager.hasAnyPermission(props.permissions)) {
        return false
      }
      
      return true
    })
    
    return () => {
      if (hasAccess.value) {
        return slots.default?.()
      } else if (props.fallback) {
        return h('div', props.fallback)
      } else {
        return null
      }
    }
  }
}

// 使用示例
<template>
  <div>
    <PermissionWrapper :roles="['admin']">
      <AdminPanel />
    </PermissionWrapper>
    
    <PermissionWrapper 
      :permissions="['user.read']"
      fallback="您没有权限查看此内容"
    >
      <UserList />
    </PermissionWrapper>
  </div>
</template>

🎯 最佳实践

1. 守卫执行顺序优化

javascript
// 优化守卫执行顺序,避免重复检查
router.beforeEach(async (to, from) => {
  // 1. 首先检查是否需要认证
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return '/login'
  }
  
  // 2. 然后检查权限
  if (to.meta.roles || to.meta.permissions) {
    if (!permissionManager.canAccess(to)) {
      return '/forbidden'
    }
  }
  
  // 3. 最后处理其他逻辑
  if (to.meta.title) {
    document.title = to.meta.title
  }
})

2. 错误处理

javascript
router.onError((error) => {
  console.error('路由错误:', error)
  
  if (error.name === 'ChunkLoadError') {
    // 处理代码分割加载失败
    window.location.reload()
  }
})

// 导航失败处理
router.afterEach((to, from, failure) => {
  if (failure) {
    console.error('导航失败:', failure)
    
    if (failure.type === NavigationFailureType.aborted) {
      // 导航被中止
    } else if (failure.type === NavigationFailureType.cancelled) {
      // 导航被取消
    } else if (failure.type === NavigationFailureType.duplicated) {
      // 重复导航
    }
  }
})

3. 性能优化

javascript
// 路由懒加载
const routes = [
  {
    path: '/admin',
    component: () => import(/* webpackChunkName: "admin" */ '@/views/Admin.vue'),
    meta: { requiresAuth: true, roles: ['admin'] }
  }
]

// 预加载关键路由
router.beforeEach((to, from) => {
  // 预加载用户可能访问的路由
  if (to.path === '/dashboard') {
    import('@/views/Profile.vue')
    import('@/views/Settings.vue')
  }
})

Vue Router的导航守卫系统为应用提供了强大的路由控制能力,结合权限管理可以构建安全可靠的前端应用。