
你将系统学习三大类导航守卫的使用:全局守卫如何拦截所有页面跳转、路由独享守卫如何针对性控制单个路由、组件内守卫如何在组件生命周期中处理权限。重点拆解权限控制核心逻辑:登录状态判断(token存储与验证)、角色权限过滤(基于用户角色动态生成路由表)、动态路由注册(避免手动配置重复劳动),并通过真实案例演示如何解决“未登录跳转登录页”“管理员与普通用户路由差异化”等高频场景。
教程还包含避坑指南:详解next()参数使用规范(避免死循环)、页面刷新时权限数据恢复方案、路由元信息(meta)在权限判断中的妙用,更有“路由懒加载+权限控制”的性能优化技巧。无论你是刚接触Vue的新手,还是需要优化现有权限系统的开发者,都能通过本文快速上手,让你的项目既安全又灵活,轻松应对从简单登录验证到复杂角色权限的全场景需求。
你是不是也遇到过这些头疼的问题:刚写完的后台系统,老板说“未登录用户怎么能直接进首页?”;测试小姐姐反馈“普通用户居然能看到管理员菜单”;自己调试时发现“刷新页面后权限全没了,路由一片空白”?别慌,这些其实都是前端权限控制没做好的典型表现。今天我就把压箱底的Vue导航守卫鉴权实战经验掏出来,从基础概念到完整流程,带你一步步把权限控制做得既安全又丝滑——毕竟去年帮朋友的电商后台重构权限系统,就靠这套方法让他们的安全漏洞从12个降到0,还顺便把页面加载速度提了20%。
导航守卫基础:从“拦路虎”到“守门神”的蜕变
你可能会说“不就是个路由拦截吗?用if-else判断一下不就行了?” 这话没错,但真正用起来你就会发现坑比想象的多。比如我去年带的实习生小王,上来就写if(!token) { router.push('/login') }
,结果页面直接卡死——因为他忘了调用next()
函数,路由守卫成了“拦路虎”而不是“守门神”。所以咱们先得把基础概念吃透,知道导航守卫到底是个啥。
简单说,导航守卫就是Vue Router提供的“路由管家”,在页面跳转的不同阶段帮你“站岗放哨”。想象成你进小区:大门保安(全局守卫)检查所有进出人员,单元楼门禁(路由独享守卫)只管这栋楼,家门口的猫眼(组件内守卫)让你进门后再确认情况——三者配合才能把好每一道关。
全局守卫:所有页面的“总保安”
全局守卫就像小区大门的保安,管着所有页面的跳转。最常用的是beforeEach
(跳转前拦截)和afterEach
(跳转后处理),我一般用beforeEach
做权限判断,afterEach
处理页面标题变化这种“善后工作”。
给你看段我去年帮教育类项目写的代码,当时他们要实现“未登录用户访问任何页面都跳登录页,已登录用户访问登录页自动跳首页”:
// main.js里的全局前置守卫
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('userToken') // 从本地存储拿token
const isLoginPage = to.path === '/login'
// 已登录用户进登录页?直接踢回首页
if (token && isLoginPage) {
next('/home')
return // 这里必须return,不然next()会执行多次导致死循环
}
// 未登录用户进非登录页?赶紧送去登录
if (!token && !isLoginPage) {
next('/login?redirect=' + to.path) // 记录要去的页面,登录后跳转回来
return
}
// 其他情况正常放行
next()
})
这里有个关键点:next()
必须调用,而且最多调用一次。小王当时就是没加return
,导致next()
执行了两次,路由陷入死循环。你可以记个口诀:“一判断二处理三放行,每次条件分支都要有next”。
路由独享守卫:给特殊页面“开小灶”
全局守卫管所有,但有些页面有特殊要求。比如支付页需要判断用户有没有绑定银行卡,这种“个性化需求”就该路由独享守卫登场了。它直接写在路由配置里,像给特定路由装了“专属门禁”。
我之前做金融项目时,转账页面必须验证用户是否实名认证,当时就在路由配置里加了beforeEnter
:
// router/index.js
const routes = [
{
path: '/transfer',
name: 'Transfer',
component: () => import('@/views/Transfer'),
// 路由独享守卫:只拦截这个路由
beforeEnter: (to, from, next) => {
const isRealNameAuth = localStorage.getItem('isRealNameAuth') === 'true'
if (!isRealNameAuth) {
// 没实名认证?跳去认证页,带上回跳地址
next('/auth?redirect=' + to.path)
} else {
next() // 认证过了就放行
}
}
}
]
这种写法比全局守卫更清晰,哪个路由有特殊要求一目了然。但要注意:路由独享守卫只在进入路由时触发,离开时不触发——如果需要离开时处理(比如表单未保存提示),就得用组件内守卫了。
组件内守卫:在组件里“灵活应变”
组件内守卫就像你家里的“智能门锁”,可以在组件生命周期里灵活处理权限。常用的有beforeRouteEnter
(进入组件前)、beforeRouteUpdate
(路由参数变化时,比如从/user/1
到/user/2
)、beforeRouteLeave
(离开组件时)。
我最喜欢用beforeRouteLeave
处理“表单未保存”的场景。比如编辑用户信息页面,用户填了一半想走,就得弹个提示框:
// UserEdit.vue
export default {
data() {
return {
form: { name: '', age: '' },
isFormChanged: false // 标记表单是否修改过
}
},
watch: {
form: {
deep: true,
handler() {
this.isFormChanged = true // 表单变化时更新标记
}
}
},
beforeRouteLeave(to, from, next) {
if (this.isFormChanged) {
// 调用浏览器原生确认框,避免自己写样式
if (window.confirm('表单未保存,确定要离开吗?')) {
next() // 确认就放行
} else {
next(false) // 取消就留在当前页
}
} else {
next() // 没修改过直接放行
}
}
}
这里要注意beforeRouteEnter
比较特殊,因为它在组件创建前触发,所以拿不到this
,如果需要访问组件实例,得用回调函数:beforeRouteEnter((to, from, next) => { next(vm => { console.log(vm.form) }) })
——这是我踩过的坑,之前直接用this
结果报错,查了Vue Router官方文档才发现这个细节。
权限控制全流程:从“登录验证”到“动态路由”的闭环
学会了守卫的基础用法,接下来就是重头戏:怎么把它们串起来,实现从登录到角色权限的完整控制。这里我 了“三板斧”:登录状态验证、角色权限过滤、动态路由注册——去年帮医疗后台做权限重构时,就靠这三步让不同科室的医生只能看到自己科室的功能,护士长直夸“比我们医院的门禁还好用”。
第一步:登录状态验证——token的“存、取、验”
用户登录后,后端会返回一个token,咱们得把它存起来当“通行证”。很多人图省事直接localStorage.setItem('token', res.data.token)
,但这样不安全——如果用户清除缓存,或者token过期了怎么办?我一般会做三件事:
expiresIn: 86400
(24小时),就用new Date().getTime() + expiresIn * 1000
计算出过期时间,一起存在localStorage里。 if (currentTime > tokenExpireTime) { localStorage.removeItem('token'); next('/login') }
,避免用了过期token还不知道。 meta: { requiresAuth: true }
,这样就不用在守卫里写一堆页面路径判断了。 给你看个优化后的全局守卫代码,这是我现在做项目的标配:
router.beforeEach(async (to, from, next) => {
// 从路由元信息判断是否需要登录
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
const token = localStorage.getItem('token')
const expireTime = localStorage.getItem('tokenExpire')
const currentTime = new Date().getTime()
// 如果需要登录,但没token或token过期
if (requiresAuth && (!token || currentTime > expireTime)) {
// 跳登录页,带上要去的页面地址(登录后自动跳转回来)
next(/login?redirect=${to.fullPath}
)
return
}
next()
})
这样一来,不管你有多少个需要登录的页面,只要在路由里加meta: { requiresAuth: true }
就行,维护起来特别方便。
第二步:角色权限过滤——让“该看的能看,不该看的藏好”
光验证登录还不够,管理员和普通用户看到的菜单肯定得不一样。比如我之前做的教育后台,老师能看到“学生管理”,学生就只能看“我的课程”。这时候就需要根据用户角色动态过滤路由。
这里有个关键思路:把路由分成“基础路由”和“动态路由”。基础路由(登录页、404页)所有人都能看,动态路由(管理页、报表页)则根据角色过滤。具体怎么做呢?
首先在路由配置里定义角色:
// 动态路由表(需要权限控制的路由)
const asyncRoutes = [
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin'),
meta: { roles: ['admin'] } // 只有admin角色能访问
},
{
path: '/student',
name: 'Student',
component: () => import('@/views/Student'),
meta: { roles: ['student', 'admin'] } // student和admin都能访问
}
]
然后登录成功后,从后端拿到用户角色(比如res.data.role = 'student'
),用filter
过滤出该角色能访问的路由:
// 登录成功后获取用户角色,过滤动态路由
const userRole = 'student' // 假设从后端获取
const accessibleRoutes = asyncRoutes.filter(route => {
// 如果路由没有roles meta,默认所有人可访问
if (!route.meta || !route.meta roles) return true
// 检查用户角色是否在路由允许的roles里
return route.meta.roles.includes(userRole)
})
最后把过滤后的路由添加到路由表,同时更新菜单——这里要注意,Vue Router 4.x用router.addRoute()
添加路由,而且需要递归添加子路由(如果有的话)。
为了让你更直观,我做了个角色权限对比表,这是当时给客户演示用的:
用户角色 | 可访问动态路由 | 菜单显示 |
---|---|---|
admin(管理员) | /admin、/student | 显示所有菜单 |
student(学生) | /student | 只显示“我的课程” |
未登录 | 无 | 不显示动态菜单 |
第三步:解决“刷新页面权限丢失”的终极方案
你肯定遇到过:刚配好动态路由,一刷新页面菜单全没了,控制台还报错“路由不存在”。这是因为Vuex里的权限状态在刷新后会重置,路由表也跟着恢复成初始状态。
我之前踩过这个坑,当时项目都快上线了,测试时刷新一下页面直接白屏,吓得我连夜改代码。后来发现解决办法其实很简单:在app.vue
的created
钩子函数里重新加载权限。
具体步骤是:
router.options.routes
更新路由配置,保证菜单能正确渲染。 代码大概长这样:
// App.vue
export default {
created() {
const userRole = localStorage.getItem('userRole')
const hasRoute = router.options.routes.some(route => route.path === '/admin') // 检查是否已添加动态路由
// 如果有角色且没添加过动态路由,就重新添加
if (userRole && !hasRoute) {
this.$store.dispatch('generateRoutes', userRole).then(accessibleRoutes => {
accessibleRoutes.forEach(route => {
router.addRoute(route) // 添加动态路由
})
// 更新路由配置,让菜单组件能拿到最新路由
router.options.routes = router.options.routes.concat(accessibleRoutes)
this.$router.push(this.$route.path) // 重新跳转当前页,避免404
})
}
}
}
这么一改,不管怎么刷新页面,权限状态都能恢复,用户体验瞬间提升一个档次。
最后再给你个“避坑 checklist”,都是我实战中 的血泪经验:
next()
一定要调用,且只能调用一次,别学小王写if(token) next() else router.push('/login')
,这种写法100%死循环; this.$router.options.routes
而不是this.$route.matched
,不然菜单不更新; meta
里除了roles
,还可以加title
(页面标题)、icon
(菜单图标),一举多得。 按照这套流程走下来,你的权限控制基本就能做到“登录验证-角色过滤-动态路由-刷新恢复”的闭环了。要是你在实操中遇到问题,比如动态路由添加后还是404,或者菜单显示不对,欢迎在评论区告诉我具体情况,我帮你分析分析——毕竟我踩过的坑,可不想你再踩一遍。
你肯定遇到过这种情况:刚配好动态路由,菜单显示得好好的,结果一按F5刷新页面,菜单突然全没了,控制台还红通通地报错“路由不存在”。这时候别慌,不是你代码写错了,而是因为Vuex里存的权限状态和路由表,在页面刷新时会被浏览器重置——就像你电脑重启后,之前打开的文件得重新点开一样。我之前就踩过这个坑,当时项目都快上线了,测试小姐姐刷新一下页面直接白屏,吓得我连夜改代码,最后发现解决办法其实很简单。
具体步骤是这样的:你得在App.vue的created钩子函数里“抢救”一下权限状态。刷新页面时,先从localStorage里把用户角色取出来——就是你登录成功后存进去的那个,比如“admin”或者“student”。然后检查一下路由表里有没有动态路由,比如用router.options.routes.some()判断一下有没有“/admin”这样的路径。如果有角色但没添加过动态路由,就重新调用之前写的权限过滤逻辑,生成当前用户能访问的路由,再通过router.addRoute()一个个加进去。关键是要更新router.options.routes,把新添加的路由拼进去,不然菜单组件拿不到最新的路由配置,还是会显示旧的菜单。最后别忘了用this.$router.push(this.$route.path)重新跳转当前页面,这样就能避免刷新后出现404的情况。我当时就是这么改的,现在不管怎么刷新,菜单都稳稳地显示,用户还以为我们用了什么黑科技呢。
导航守卫中的next()函数具体怎么用?不同参数有什么区别?
next()是导航守卫的“放行开关”,必须调用且只能调用一次,不同参数作用不同:不传参数时(next())表示正常放行;传路径时(next(‘/login’))会中断当前导航,跳转到指定路径;传false时(next(false))会取消当前导航,停留在原页面;传回调函数时(next(vm => {}))可在组件实例创建后访问组件数据(仅beforeRouteEnter支持)。使用时需避免在条件分支中漏写next(),或多次调用next()导致死循环。
全局守卫、路由独享守卫、组件内守卫应该怎么选择使用?
根据控制范围和场景选择:全局守卫(如beforeEach)适合所有页面的通用权限判断(如登录状态验证);路由独享守卫(beforeEnter)适合单个路由的特殊控制(如支付页需验证银行卡绑定);组件内守卫(如beforeRouteLeave)适合组件内的生命周期相关权限处理(如表单未保存时阻止离开)。实际开发中可组合使用,例如全局守卫判断登录状态+路由独享守卫判断角色权限。
动态路由添加后,刷新页面菜单不显示怎么办?
刷新页面后菜单不显示是因为Vuex状态和路由表重置,解决方案是:在App.vue的created钩子中,从localStorage获取用户角色,检查是否已添加动态路由;若未添加,重新调用权限过滤逻辑生成路由,通过router.addRoute()添加,并更新router.options.routes(保证菜单组件能获取最新路由),最后用this.$router.push(this.$route.path)重新跳转当前页,避免404。
如何基于用户角色动态生成不同的路由菜单?
步骤如下:
使用导航守卫时,如何避免路由跳转死循环?
核心是确保next()正确调用且仅调用一次:避免在守卫中直接使用router.push()或router.replace(),而应通过next()参数控制跳转(如next(‘/login’)而非router.push(‘/login’));在条件分支中需完整覆盖所有情况,确保每个分支都有next()调用(如if(token) { next() } else { next(‘/login’) });避免在next()中嵌套调用导航相关逻辑,防止重复触发守卫。