跟着做完这几步,你就能让未登录用户进不去后台、让不同角色看到不同的菜单路由。
第一步:用 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 通配路由永远最后加