几乎所有需要用户身份的应用都会面临两个核心问题:“没登录不能进某些页面”和“不同角色能看到不同的菜单/页面”。Vue Router 提供的路由守卫是解决这两个问题的标准切入点——它能在每次路由跳转前拦截请求,执行校验逻辑。
下面是一套在实际项目中可落地的标准实现,按“基础登录拦截”到“角色权限控制”的顺序展开。
一、统一存储 Token
用户登录后,服务端通常会返回一个 token(如 JWT),前端需要将其持久化,以便每次请求携带。常用做法是存 localStorage(持久登录)或 sessionStorage(关闭浏览器即失效),同时封装成工具函数方便调用:
// utils/auth.js
const TOKEN_KEY = 'app_token'
export function getToken() {
return localStorage.getItem(TOKEN_KEY)
}
export function setToken(token) {
localStorage.setItem(TOKEN_KEY, token)
}
export function removeToken() {
localStorage.removeItem(TOKEN_KEY)
}
二、路由配置:用 meta 标记权限
在定义路由表时,利用 meta 字段为每条路由附加权限信息,例如是否需要登录、允许哪些角色访问:
// router/index.js
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { requiresAuth: false } // 明确标记无需登录
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true, roles: ['admin', 'editor'] }
},
{
path: '/settings',
name: 'Settings',
component: () => import('@/views/Settings.vue'),
meta: { requiresAuth: true, roles: ['admin'] }
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
}
]
三、全局前置守卫:统一登录拦截
在 router.beforeEach 中编写核心校验逻辑。每次路由变化,先检查目标路由是否需要认证,再判断当前是否存在 token,最后校验角色权限:
import { createRouter } from 'vue-router'
import { getToken } from '@/utils/auth'
const router = createRouter({ ... })
router.beforeEach((to, from, next) => {
const token = getToken()
// 1. 如果目标路由需要登录
if (to.meta.requiresAuth !== false) {
if (!token) {
// 没有 token,跳转登录页,并将当前目标路径通过 query 传递,方便登录后回跳
next({ path: '/login', query: { redirect: to.fullPath } })
return
}
// 2. 如果路由定义了角色限制
if (to.meta.roles && to.meta.roles.length > 0) {
const userRoles = getUserRoles() // 从本地存储或状态管理中获取当前用户角色列表
const hasRole = to.meta.roles.some(role => userRoles.includes(role))
if (!hasRole) {
// 无权限,可以跳转到 403 页面或统一提示
next('/403')
return
}
}
}
// 3. 已经登录却访问登录页,直接重定向到首页(可选优化)
if (token && to.path === '/login') {
next('/')
return
}
next()
})
关键细节说明:
redirect参数:登录成功后,从route.query.redirect取值并跳转,或默认去首页,用户体验大幅提升。- 角色信息获取:
getUserRoles()通常从 Pinia store 或localStorage中读取登录时保存的用户角色,防止刷新后丢失。 - 白名单:如 404 页面、注册页等不需要拦截的路由,
meta.requiresAuth设false或干脆不设该字段(代码中按需处理)。 - 异步校验:如果角色信息需要异步拉取(比如首次进入时请求用户详情接口),可以在守卫中 await,此时守卫支持返回 Promise。
四、登录成功后获取用户信息
登录页调用接口拿到 token 后,除了存储 token,一般还会立即请求用户详情(角色、菜单权限等),并存入状态管理,以便全局守卫使用:
// views/Login.vue 中
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
async function handleLogin() {
const res = await loginApi({ username, password })
setToken(res.data.token)
await userStore.fetchUserInfo() // 拉取用户角色、权限菜单等
router.push(route.query.redirect || '/')
}
五、按钮级权限控制(可选扩展)
除了路由层面,页面内的按钮也常需要根据角色显示或隐藏。可以封装一个自定义指令或组件:
指令方式:
// directives/permission.js
import { useUserStore } from '@/stores/user'
export default {
mounted(el, binding) {
const { value } = binding // 传入允许的角色数组,如 ['admin']
const userStore = useUserStore()
if (value && value.length > 0) {
const hasPermission = value.some(role => userStore.roles.includes(role))
if (!hasPermission) {
el.parentNode?.removeChild(el) // 直接移除 DOM
}
}
}
}
在组件中使用:
<button v-permission="['admin']">删除用户</button>
六、真实项目中的稳定姿态
- 路由守卫不做重逻辑:避免在守卫里做大量接口调用或复杂计算,否则白屏等待时间过长。用户信息拉取应尽量在登录时完成并缓存。
- 与后端接口鉴权结合:前端权限控制仅做 UI 层面的友好拦截,真正的安全边界在服务端。每个需要权限的接口都需要后端二次校验。
- 动态路由生成:当权限粒度细到“每个用户看到的菜单不同”,建议用
router.addRoute根据后端返回的菜单树动态生成路由,这部分可参考路由章节的“动态路由添加”部分。
这套方案覆盖了从登录拦截到角色权限控制的核心链路,足够应付 90% 的中后台项目。它的优势在于逻辑集中、可维护性强——所有跳转规则都收敛在路由守卫中,新增权限页面只需修改路由配置的 meta 字段即可,不会让权限判断散落在各个组件里。