跟着做完这几步,你就能让未登录用户进不去后台、让不同角色看到不同的菜单路由。
第一步:用 meta 给路由打标记
鉴权的关键是「哪些页面需要登录」,最省事的做法是写在路由配置的 meta 里,而不是在每个页面组件里写判断。
const routes = [
{ path: '/', component: Home },
{ path: '/login', component: Login },
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true }
},
{
path: '/admin',
component: Admin,
meta: { requiresAuth: true, roles: ['admin'] }
},
// 兜底必须放最后
{ path: '/:pathMatch(.*)*', component: NotFound }
]
meta 是自由字段,你可以约定 requiresAuth(要不要登录)、roles(谁能进)、title(页面标题),守卫里统一读。
注意:Vue Router 3 写的是 path: '*',Vue Router 4 必须写成 /:pathMatch(.*)*,照抄老教程会直接匹配不到。
第二步:写全局前置守卫
在 router/index.js 里注册 beforeEach,它是所有跳转的必经关口:
router.beforeEach((to, from) => {
const token = localStorage.getItem('token')
// 去登录页直接放行,避免死循环
if (to.path === '/login') return true
// 需要登录但没 token
if (to.meta.requiresAuth && !token) {
return { path: '/login', query: { redirect: to.fullPath } }
}
// 角色不匹配
if (to.meta.roles && !to.meta.roles.includes(store.user.role)) {
return { path: '/403' }
}
return true
})
return true 放行、return { path } 跳走,也可以 return false 直接取消导航。
注意:Vue Router 4 里 next() 仍然能用,但不要和 return 混用,同一个守卫里 return 了又调 next() 会报 "next is called multiple times"。新项目建议全程用 return 风格。
第三步:登录后跳回原页面
上面把目标地址塞进了 query.redirect,登录页读出来用完再跳:
const route = useRoute()
const router = useRouter()
async function handleLogin() {
await api.login(form)
const target = route.query.redirect || '/'
router.replace(target) // 用 replace,避免用户回退又回到登录页
}
注意:redirect 来自 URL,属于用户可控输入。跳之前校验一下是不是以 / 开头的站内路径,否则会变成开放重定向漏洞。
第四步:动态路由按权限下发
如果后台菜单完全由角色决定(比如 A 角色能看订单、B 角色能看财务),把路由也做成动态的:
const asyncRoutes = {
admin: [
{ path: 'users', name: 'Users', component: () => import('@/views/Users.vue') }
],
editor: [
{ path: 'posts', name: 'Posts', component: () => import('@/views/Posts.vue') }
]
}
function addRoutes(role) {
const list = asyncRoutes[role] || []
list.forEach(r => router.addRoute('Layout', r)) // 'Layout' 是父路由 name
}
登录成功后调用 addRoutes(user.role),再 router.replace(redirect)。
注意:addRoute 之后当前这次导航不会自动重新匹配。如果你是在守卫内部加的(比如刷新页面场景),必须 return to.fullPath 再触发一次导航,否则会停在空白页。
第五步:登出重置 + 刷新恢复
动态路由存在内存里,刷新就没了,登出也必须清掉:
function resetRouter() {
asyncRoutes[store.user.role]?.forEach(r => {
if (router.hasRoute(r.name)) router.removeRoute(r.name)
})
}
登出时先 resetRouter() 再跳登录页。而刷新页面时,在守卫里补一段恢复逻辑:
router.beforeEach(async (to) => {
const token = localStorage.getItem('token')
if (token && !store.userLoaded) {
await store.fetchUserInfo() // 拉用户信息拿角色
addRoutes(store.user.role)
router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' })
return to.fullPath // 重新匹配一次
}
// ...下面再做鉴权判断
})
注意:兜底 404 通配路由要在动态路由加完之后再 add。如果它先注册,动态路由后加也没用——通配符会把它们的匹配提前吃掉。
小结
- 用
meta.requiresAuth / meta.roles 标记权限,守卫里统一读,别散落在组件里
- 全局用
beforeEach,返回 true 放行、返回路径跳转,别和 next() 混用
- 拦截时把
to.fullPath 存进 query.redirect,登录后 replace 回去
- 动态路由靠
addRoute 下发,加完要 return to.fullPath 重新触发匹配
- 登出用
removeRoute 清理,刷新要靠拉取用户信息重新挂载
- 404 通配路由永远最后加