Vue Router 怎么实现登录权限控制?
做 Vue 项目时,不少同学都会碰到这样的需求:没登录的用户不能进个人中心、订单页这些私密页面;管理员和普通用户能访问的页面还不一样,这时候就得靠 Vue Router 的登录权限控制来实现,可具体咋操作?从基础逻辑到细节处理,咱一步步拆解清楚。
先理清登录权限控制的核心逻辑
权限控制本质是「拦请求 + 验身份 + 分权限」的组合拳,举个例子:用户想访问 /profile
(个人中心),Vue Router 得先“拦住”这个请求,检查用户有没有登录、是不是有权限,再决定让不让进,核心靠这三部分配合:
- 路由守卫:相当于项目里的“门卫”,所有页面跳转前先检查资格;
- 身份验证:一般用 Token(令牌)判断用户是否登录,Token 可以存在 LocalStorage、SessionStorage 这类浏览器存储里;
- 权限标记:给不同页面贴“标签”(比如哪些页面需要登录、哪些只有管理员能进),用路由的
meta
字段实现。
举个简单场景:用户点「我的订单」,路由守卫先查有没有 Token,没有就踢去登录页;有 Token 但订单页要求是 VIP 用户,还得查用户角色对不对,不对就跳 403 页面。
路由守卫是怎么拦截请求的?
Vue Router 提供了三种守卫:全局守卫、路由独享守卫、组件内守卫,开发里用得最多的是全局守卫 router.beforeEach
,因为它能拦所有页面跳转。
(1)全局守卫:拦所有路由跳转
全局守卫是“全局门卫”——项目里所有页面跳转前,都会触发这个守卫,代码长这样:
// router/index.js const router = createRouter({ history: createWebHistory(), routes: [...] }) router.beforeEach((to, from, next) => { // to:要跳转到的目标路由(包含路径、参数、meta 等信息) // from:从哪个路由跳过来的 // next:决定往哪跳的函数,必须调用!不调用路由会卡住 const isLogin = localStorage.getItem('token') // 假设登录成功后,把 token 存在 LocalStorage // 先看目标路由需不需要登录(用 meta.requiresAuth 标记) if (to.meta.requiresAuth) { if (isLogin) { next() // 已登录,放行 } else { next('/login') // 没登录,踢去登录页 } } else { next() // 不需要登录的页面,直接放行 } })
然后得给需要登录的路由加 meta
标记,比如路由配置:
const routes = [ { path: '/profile', name: 'Profile', component: () => import('@/views/Profile.vue'), meta: { requiresAuth: true } // 标记这个页面需要登录 }, { path: '/login', name: 'Login', component: () => import('@/views/Login.vue') } ]
这样用户访问 /profile
时,全局守卫就会检查有没有 Token,没有就跳登录页。
(2)路由独享守卫:只拦单个路由
如果某个页面(比如管理员后台)想单独设置守卫,不用全局守卫,可以用 beforeEnter
,比如管理员页面不仅要登录,还得验证角色:
{ path: '/admin', name: 'Admin', component: () => import('@/views/Admin.vue'), meta: { requiresAuth: true }, beforeEnter: (to, from, next) => { const userRole = localStorage.getItem('role') // 假设登录时存了用户角色 if (userRole === 'admin') { next() // 是管理员,放行 } else { next('/403') // 不是管理员,跳无权访问页 } } }
这种守卫只对 /admin
生效,适合页面权限更特殊的情况(比如只有超级管理员能进)。
(3)组件内守卫:组件里自己拦
如果想在组件内部处理权限(比如进入组件后,再判断用户有没有操作某个按钮的权限),可以用 beforeRouteEnter
(进入组件前触发),比如在 Profile.vue
里:
export default { name: 'Profile', beforeRouteEnter(to, from, next) { const isLogin = localStorage.getItem('token') if (!isLogin) { next('/login') // 没登录就跳登录页 } else { next() // 已登录放行 } } }
不过这种方式不如全局守卫灵活,一般用来做组件内的“额外验证”(比如进组件后,再查用户有没有编辑个人信息的权限)。
Token 该存在哪?怎么验证有效性?
Token 是判断用户是否登录的核心凭证,存哪、咋验证,得结合项目需求选。
(1)Token 的存储方式
常见的存储位置有三个,各有优劣:
- LocalStorage:关闭浏览器也不会清除,适合“长期保持登录状态”的场景;但容易被 XSS 攻击(简单说就是黑客往页面插恶意脚本,偷 LocalStorage 里的 Token),所以别存密码这类敏感信息,只存 Token 就行。
- SessionStorage:关闭标签页就清除,适合“临时登录”的场景;但页面刷新后还在,新开标签页就没了,比如做个临时预览功能,用 SessionStorage 存 Token 很合适。
- Cookie:可以设置
httponly
防止 XSS,但前端拿 Cookie 麻烦,还可能有 CSRF 风险(比如黑客诱导用户点恶意链接,冒用 Cookie 发请求)。
大部分项目选 LocalStorage 存 Token,登录成功后存一下:
// 登录接口成功后 axios.post('/login', { username, password }).then(res => { const token = res.data.token localStorage.setItem('token', token) // 把 Token 存起来 router.push('/profile') // 跳个人中心 })
(2)验证 Token 是否有效
存了 Token 不代表永远有效,得定期验证,常见做法有两种:
- 全局守卫里“主动”验证:每次跳转前,用 Token 调后端接口(
/api/verify
),看是否过期,但这样每次跳转都发请求,影响性能,所以一般只在「页面刷新」或「登录后首次跳转」时验证。 - 响应拦截器“被动”处理:后端接口返回 401(未授权)时,说明 Token 过期,直接跳登录页并清除 Token:
// axios 响应拦截器 axios.interceptors.response.use( response => response, // 响应正常,直接返回 error => { if (error.response.status === 401) { localStorage.removeItem('token') // 清除过期 Token router.push('/login') // 跳登录页,让用户重新登录 } return Promise.reject(error) // 把错误抛出去,让业务代码处理 } )
这样用户操作时,接口返回 401 就自动跳登录,体验更流畅。
动态加载路由实现细粒度权限
有些项目需要“不同角色看不同页面”,比如管理员能进用户管理,普通用户不能,这时候得用 动态路由——登录后根据角色加载对应路由。
(1)定义不同角色的路由表
先写好管理员和普通用户的路由(比如管理员能看用户管理,普通用户不能):
// 管理员专属路由 export const adminRoutes = [ { path: '/user-manage', name: 'UserManage', component: () => import('@/views/Admin/UserManage.vue') } ] // 普通用户路由(假设没有额外页面,这里只是示例) export const userRoutes = []
(2)登录后动态添加路由
用户登录成功后,后端返回角色(res.data.role
),前端根据角色加路由:
// 登录接口成功后 axios.post('/login', { username, password }).then(res => { const { token, role } = res.data localStorage.setItem('token', token) localStorage.setItem('role', role) // 存角色信息,后续验证用 // 根据角色加载对应路由 const asyncRoutes = role === 'admin' ? adminRoutes : userRoutes asyncRoutes.forEach(route => { router.addRoute(route) // 动态添加路由 }) router.push('/') // 跳首页,让动态路由生效 })
(3)处理动态路由的刷新问题
页面刷新后,动态添加的路由会“消失”(因为 Vue 重新初始化了),所以得在 全局守卫 里重新加载动态路由:
router.beforeEach((to, from, next) => { const isLogin = localStorage.getItem('token') const role = localStorage.getItem('role') // 检查是否已添加动态路由(比如看 /user-manage 是否存在) const hasDynamicRoutes = router.getRoutes().some(route => route.path === '/user-manage') if (isLogin && !hasDynamicRoutes) { // 已登录但没加动态路由,重新加 const asyncRoutes = role === 'admin' ? adminRoutes : userRoutes asyncRoutes.forEach(route => { router.addRoute(route) }) next({ ...to, replace: true }) // 重新跳转,确保路由生效 } else { // 其他情况正常处理(比如检查是否需要登录) if (to.meta.requiresAuth && !isLogin) { next('/login') } else { next() } } })
这样刷新页面后,动态路由会重新加载,用户不会看到 404。
登录状态持久化咋处理?
页面刷新时,Vue 实例会重新创建,之前存在内存里的登录状态会丢失,这时候得靠 LocalStorage/SessionStorage 存 Token,刷新后再读出来验证。
(1)刷新页面时恢复登录状态
在全局守卫里,页面刷新后先检查 LocalStorage 的 Token,再验证有效性:
router.beforeEach((to, from, next) => { const isLogin = localStorage.getItem('token') if (to.meta.requiresAuth) { if (isLogin) { // 调后端接口验证 Token 是否真的有效(防止 Token 伪造) axios.get('/api/verify').then(res => { if (res.data.valid) { next() // Token 有效,放行 } else { localStorage.removeItem('token') // 清除无效 Token next('/login') // 跳登录页 } }) } else { next('/login') // 没 Token,跳登录页 } } else { next() // 不需要登录的页面,直接放行 } })
这样即使页面刷新,只要 Token 有效,用户还能保持登录状态。
(2)退出登录时清除状态
退出登录功能要彻底清除 Token 和角色信息,避免“退出后还能访问私密页面”:
// 退出登录按钮逻辑 const handleLogout = () => { localStorage.removeItem('token') // 清除 Token localStorage.removeItem('role') // 清除角色信息 router.push('/login') // 跳登录页 // 可选:调后端接口,让 Token 失效(比如加入黑名单) axios.post('/logout') }
处理特殊场景:登录页重复跳转、权限错配
实际开发中,总有一些“意外情况”,得提前处理。
(1)已登录还跳登录页?
用户已经登录了,还点「登录」按钮,或者手动输 /login
网址,这时候得跳首页,别让他重复进登录页:
router.beforeEach((to, from, next) => { const isLogin = localStorage.getItem('token') // 如果要去登录页,但用户已登录 if (to.path === '/login' && isLogin) { next('/') // 直接跳首页 return // 终止后续逻辑 } // 其他权限验证逻辑... })
(2)权限错配导致 404?
比如用户是普通用户,却手动输管理员页面地址 /admin
,前端路由守卫没拦住(meta 配置错了),这时候后端接口也会返回 403,前端要跳 403 页面:
// 路由配置里加 403 页面 { path: '/403', name: 'Forbidden', component: () => import('@/views/403.vue') } // axios 响应拦截器 axios.interceptors.response.use( response => response, error => { if (error.response.status === 403) { router.push('/403') // 跳无权访问页 } return Promise.reject(error) } )
(3)动态路由没加载导致白屏?
动态路由添加后,第一次访问可能因为路由还没加载完导致白屏,解决方法是用 next({ ...to, replace: true })
强制刷新路由:
// 动态添加路由后 asyncRoutes.forEach(route => { router.addRoute(route) }) next({ ...to, replace: true }) // 替换当前跳转,确保路由生效
结合后端接口做权限验证
前端控制路由只是“表面功夫”,后端必须再做一层验证!比如用户伪造 Token 访问管理员接口,前端拦不住,后端得检查 Token 里的角色权限。
(1)请求头带 Token
每次发请求时,把 Token 放到请求头里,让后端验证:
// axios 请求拦截器 axios.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` // 把 Token 塞到请求头 } return config })
(2)后端验证接口权限
后端接收到请求后,先解析 Token,检查用户角色是否能访问该接口。/api/user-manage
接口,只有管理员角色能访问,后端返回:
- 200:有权限,返回数据;
- 403:没权限,返回错误;
- 401:Token 过期或无效。
前端再根据后端返回的状态码处理(403 跳 403 页面,401 跳登录页)。
实战案例:后台管理系统权限控制
假设做一个电商后台,有「普通运营」和「超级管理员」两种角色:
- 普通运营:能看订单列表、商品列表;
- 超级管理员:能看用户管理、系统设置,还能操作订单和商品。
(1)路由配置(基础 + 动态)
基础路由(不需要权限的页面):
const basicRoutes = [ { path: '/', redirect: '/order' }, // 首页重定向到订单页 { path: '/login', component: Login }, // 登录页 { path: '/403', component: Forbidden } // 无权访问页 ]
动态路由(按角色分):
// 运营路由(普通运营能看的页面) export const operatorRoutes = [ { path: '/order', component: Layout, // 布局组件,包含侧边栏、头部 children: [ { path: '', component: OrderList, meta: { requiresAuth: true } } ] }, { path: '/product', component: Layout, children: [ { path: '', component: ProductList, meta: { requiresAuth: true } } ] } ] // 管理员路由(超级管理员能看的页面,包含运营的所有页面) export const adminRoutes = [ ...operatorRoutes, // 运营能看的,管理员也能看 { path: '/user', component: Layout, children: [ { path: '', component: UserManage, meta: { requiresAuth: true, roles: ['admin'] } } ] }, { path: '/setting', component: Layout, children: [ { path: '', component: SystemSetting, meta: { requiresAuth: true, roles: ['admin'] } } ] } ]
(2)登录逻辑 + 动态路由
登录页调接口,拿到角色后加路由:
// Login.vue const handleLogin = () => { axios.post('/login', { username
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。