Vue Router 路由守卫配置:登录状态校验与权限控制技术文档(增强版)

一、项目概述

一个基于 Vue Router 的路由守卫机制实现了前端登录状态校验与权限控制,确保未授权用户无法访问受保护资源,同时结合 NProgress 进度条 优化页面跳转体验,根据用户权限动态展示可访问菜单。

  1. 路由实例(router)

    作为整个路由系统的核心载体,提供路由守卫(beforeEachafterEach)的配置入口,是实现权限控制和页面跳转逻辑的基础。

  2. 登录状态校验工具(getToken)

    从本地存储(如 cookie、localStorage)中读取用户登录凭证(token),作为判断用户是否已登录的核心依据,是权限控制的前置条件。

  3. 用户体验优化插件(NProgress)

    页面跳转时显示进度条的轻量级插件,通过start()done()方法在路由守卫中控制进度条状态,直观反馈页面加载过程,提升用户体验。

  4. 缓存工具(useCache)

    提供本地缓存能力(如wsCache),用于持久化存储用户权限列表、用户信息等关键数据,避免页面刷新后数据丢失,支持权限逻辑的跨页面复用。

  5. 权限接口(getUserPermission)

    后端接口封装函数,用于获取当前登录用户的权限列表(如可访问的菜单、操作权限),为前端动态渲染菜单和控制页面访问权限提供数据支撑。

二、核心模块解析

2.1 路由守卫与 NProgress 进度条实现(src/permission.js)

路由守卫是权限控制的核心,通过 router.beforeEach 全局前置守卫实现登录状态与权限的校验逻辑,配合 router.afterEach 处理页面跳转后的收尾工作,其中 NProgress 进度条 用于可视化展示跳转过程,提升用户体验。

javascript 复制代码
import router from '@/router'
import { getToken } from '@/utils/auth'
import NProgress from 'nprogress' // 引入NProgress库
import 'nprogress/nprogress.css' // 引入默认样式
import { CACHE_KEY, useCache } from '@/hooks/useCache'
import { getUserPermission } from '@/api/login'

// 配置NProgress:隐藏右上角加载动画
NProgress.configure({ showSpinner: false })

// 白名单页面(无需登录即可访问)
const whiteList = ['/login', '/register']
const { wsCache } = useCache()
router.beforeEach((to, from, next) => {
  // 路由跳转开始时启动进度条
  NProgress.start()
  const hasToken = getToken() // 判断是否存在登录令牌
  if (hasToken) {
    // 已登录状态
    if (to.path === '/login' || isWhiteList(to.path)) {
      // 已登录用户访问白名单页面,重定向到首页
      next({ path: '/' })
      // 此处无需手动调用NProgress.done(),最终会由afterEach统一处理
    } else {
      // 访问受保护页面:获取用户权限并缓存
      getUserPermission({}).then(res => {
        // 缓存权限与用户信息
        wsCache.set(CACHE_KEY.PERMISSION, res.data.menuRespVOS.map(item => item.permission))
        wsCache.set(CACHE_KEY.USER, res.data.employeeUserRespVO)
        next() // 允许跳转
      }).catch(() => {
        // 权限获取失败(如token过期),清除缓存并跳转到登录页
        wsCache.delete(CACHE_KEY.PERMISSION)
        next(`/login?redirect=${encodeURIComponent(to.fullPath)}`)
      })
    }
  } else {
    // 未登录状态
    if (isWhiteList(to.path)) {
      // 允许访问白名单页面
      next()
    } else {
      // 重定向到登录页,并记录目标路径(登录后跳转回原页面)
      next(`/login?redirect=${encodeURIComponent(to.fullPath)}`)
    }
  }
})

// 路由跳转完成后结束进度条
router.afterEach(() => {
  NProgress.done()
})

// 白名单判断工具函数
const isWhiteList = (path) => {
  return whiteList.some((pattern) => {
    return path === pattern || path.startsWith(pattern + '/')
  })
}

2.2 路由配置(src/router/index.js)

javascript 复制代码
import {
  createRouter,      // 创建路由实例的核心函数
  createWebHistory   // 用于创建 HTML5 History 模式的路由(无 hash 前缀)
} from 'vue-router'
import Login from '@/views/login/index.vue'  // 登录页面组件
const Layout = () => import('@/layout/Layout.vue')  // 布局组件(采用懒加载优化性能)

// 导入菜单图标资源
import iconDevice from '@/assets/images/menu/icon-device.png'
import iconBackup from '@/assets/images/menu/icon-backup.png'

// 导出路由配置数组(供权限控制、菜单生成等场景复用)
export const routes = [
  {
    path: '/login',          // 登录页路由路径
    name: 'Login',           // 路由唯一标识(用于编程式导航)
    component: Login,        // 关联的页面组件
    meta: { 
      hidden: true           // 元信息:标记为在侧边栏菜单中隐藏
    }
  },
  {
    path: '/',               // 根路径
    redirect: '/device/cloudphone',  // 重定向到默认首页(我的云机)
    meta: { hidden: true }   // 重定向路由无需在菜单显示
  },
  {
    path: '/device',         // 设备管理模块根路径
    name: 'Device',          // 模块唯一标识
    meta: { 
      title: '设备管理',     // 菜单显示名称
      breadcrumbNoLink: true // 面包屑配置:该节点不显示链接
    },
    icon: iconDevice,        // 菜单图标
    component: Layout,       // 使用布局组件(包含侧边栏、头部等公共部分)
    children: [              // 子路由(对应子菜单)
      {
        path: 'cloudphone',  // 子路由路径(完整路径为 /device/cloudphone)
        name: 'DeviceCloudphone',
        component: () => import('@/views/device/cloudphone/index.vue'), // 懒加载子页面组件
        meta: { title: '我的云机' },  // 子菜单显示名称
        icon: iconCloudPhone          // 子菜单图标
      },
    ....
    ]
  },
  ....
  {
    path: '/:pathMatch(.*)*', // 通配符路由,匹配所有未定义的路径
    name: 'NotFound',
    component: () => import('@/views/error/404.vue'), // 404错误页面
    meta: { hidden: true }
  }
]

// 创建路由实例
const router = createRouter({
  history: createWebHistory(),  // 使用HTML5 History模式(需要后端配合配置)
  routes                        // 应用路由配置
})

export default router  // 导出路由实例供Vue应用使用

2.3 权限辅助模块(src/layout/Layout.vue

javascript 复制代码
// 从缓存获取用户信息和权限
const { wsCache } = useCache()
const userinfo = wsCache.get(CACHE_KEY.USER)
const permissions = wsCache.get(CACHE_KEY.PERMISSION)

// 原始路由配置(从 router/index.js 导入)
import { routes } from '@/router'

// 过滤路由函数:根据用户权限和角色筛选可见菜单
const filterRoutes = (routes) => {
  return routes.filter(route => {
    // 1. 过滤隐藏的路由(meta.hidden = true)
    if (route.meta?.hidden) return false
    
    // 2. 处理子路由(递归过滤)
    if (route.children && route.children.length) {
      route.children = filterRoutes(route.children)
      // 若子路由过滤后为空,父路由也不显示
      if (route.children.length === 0) return false
    }
    
    // 3. 基于用户角色的权限过滤
    // 示例1:非主账号隐藏个人中心
    if (route.path === '/center' && userinfo?.masterFlag === 0) {
      return false
    }
    
    // 示例2:员工管理仅特定角色可见
    if (route.path === '/device/staff' && userinfo?.positionType === 30) {
      return false
    }
    
    // 4. 基于权限码的过滤(如需要精确到按钮级权限)
    if (route.meta?.permission && !permissions?.includes(route.meta.permission)) {
      return false
    }
    
    return true
  })
}

// 生成最终显示的菜单
const menuItems = filterRoutes(routes)

2.4 权限指令(src/directives/index.js

javascript 复制代码
// 权限指令核心逻辑
import { useCache, CACHE_KEY } from '@/hooks/useCache'

// 判断是否拥有指定权限
const hasPermission = (permission) => {
  const { wsCache } = useCache()
  const permissions = wsCache.get(CACHE_KEY.PERMISSION) || []
  return permissions.includes(permission)
}

// 判断是否拥有指定角色
const hasRole = (role) => {
  const { wsCache } = useCache()
  const userinfo = wsCache.get(CACHE_KEY.USER) || {}
  return userinfo.roles?.includes(role)
}

// 注册权限指令:v-hasPermi
export const setupHasPermi = (app) => {
  app.directive('hasPermi', {
    mounted(el, binding) {
      const { value } = binding
      if (!hasPermission(value)) {
        // 无权限时移除元素或隐藏
        el.parentNode?.removeChild(el)
        // 或 el.style.display = 'none'
      }
    }
  })
}

// 注册角色指令:v-hasRole
export const setupHasRole = (app) => {
  app.directive('hasRole', {
    mounted(el, binding) {
      const { value } = binding
      if (!hasRole(value)) {
        el.parentNode?.removeChild(el)
      }
    }
  })
}

// 在main.js中注册
import { setupHasPermi, setupHasRole } from '@/directives'
const app = createApp(App)
setupHasPermi(app)
setupHasRole(app)

2.5 使用场景示例

javascript 复制代码
<!-- 仅拥有 device:delete 权限的用户可见 -->
<el-button v-hasPermi="'device:delete'">删除设备</el-button>

<!-- 仅管理员角色可见 -->
<el-button v-hasRole="'admin'">管理员操作</el-button>

三、常见问题及解决方案

4.1 进度条不显示或样式异常

  • 问题原因
    • 未引入 nprogress.css 或引入路径错误
    • 自定义 CSS 冲突导致进度条被隐藏
  • 解决方案
    1. 确认 import 'nprogress/nprogress.css' 语句存在且路径正确
    2. 检查全局 CSS 中是否有覆盖 #nprogress 相关样式的代码,必要时使用 !important 强制应用样式

4.2 进度条卡在某个状态不消失

  • 问题原因
    • beforeEach 守卫中未调用 next(),导致路由跳转未完成,afterEach 不执行
    • 异步操作(如 getUserPermission)未正确处理异常,导致 next() 未被调用
  • 解决方案
    1. 确保所有分支逻辑中都调用了 next()(包括 thencatch 回调)
    2. 对异步操作添加超时处理,避免因接口无响应导致进度条一直显示:
javascript 复制代码
// 示例:为getUserPermission添加超时控制
const timeoutPromise = new Promise((_, reject) => {
  setTimeout(() => reject(new Error('请求超时')), 10000)
})

Promise.race([getUserPermission({}), timeoutPromise])
  .then(res => { ... })
  .catch(err => { ... })

4.3 频繁跳转时进度条闪烁

  • 问题原因 :短时间内多次触发路由跳转(如快速点击多个菜单),导致 start()done() 频繁调用
  • 解决方案
    1. 在路由守卫中添加防抖逻辑,限制短时间内的跳转频率

    2. 保留默认行为,因 NProgress 内部已做优化,多次调用 start() 不会重复创建进度条

四、总结

本项目通过 Vue Router 路由守卫结合 NProgress 进度条,实现了兼具安全性和用户体验的权限控制体系。其中 NProgress 的引入显著提升了页面跳转过程的透明度,使用户能够清晰感知操作状态。核心亮点包括:

  1. 权限与体验并重:在严格的登录校验和权限控制基础上,通过进度条优化用户体验
  2. 轻量高效:NProgress 体积小(约 3KB),无冗余依赖,不影响页面性能
  3. 可定制化:支持通过配置项和 CSS 自定义进度条样式,适配项目主题