嘿,朋友!咱们今天不聊那些枯燥的理论,直接切入正题。你是不是也遇到过这种情况:明明用户没登录,却能在地址栏手动输入 /admin/dashboard 强行进入后台?或者更糟糕的是,一个普通用户通过修改前端代码,把自己伪装成管理员看到了不该看的数据?这简直就是安全漏洞的“直通车”。
别担心,今天我就带你用最扎实的方式,把 Vue Router 的导航守卫(Navigation Guards)和路由元信息(Meta)结合起来,打造一个既智能又安全的权限控制系统。咱们不仅要实现“未登录跳转”,还要搞定“权限不足拦截”,甚至还要考虑怎么让用户体验丝滑,而不是弹出一堆冷冰冰的错误提示。
第一步:理清思路,我们需要什么?
在写代码之前,咱们得先想清楚几个核心问题,这样写出来的代码才有条理,不会乱成一锅粥。
- 身份验证:我怎么知道用户登录了还是没登录?通常我们会把 token 存在
localStorage或sessionStorage里。 - 白名单机制:有些页面是不需要登录就能看的,比如登录页、注册页、关于我们。这些页面必须放行。
- 权限分级:登录后,不同角色能看到不同的菜单。比如
admin可以看所有页面,user只能看个人中心和首页。这就需要用到路由的meta字段。 - 全局守卫:
router.beforeEach是我们的守门员,所有的跳转请求都得经过它检查。
第二步:定义路由结构,埋下伏笔
首先,我们要在配置路由的时候,就为每个需要权限控制的页面打上标签。这就是 meta 信息的用武之地。
假设我们有三个页面:
- Login: 登录页,无需权限。
- Home: 首页,登录后可见。
- AdminPanel: 管理面板,只有管理员可见。
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Login from '../views/Login.vue'
import AdminPanel from '../views/AdminPanel.vue'
import UserCenter from '../views/UserCenter.vue'
const routes = [
{
path: '/login',
name: 'Login',
component: Login,
meta: { requiresAuth: false } // 明确标记:不需要登录
},
{
path: '/',
name: 'Home',
component: Home,
meta: {
requiresAuth: true, // 需要登录
roles: ['admin', 'user'] // 允许 admin 和 user 角色访问
}
},
{
path: '/admin',
name: 'AdminPanel',
component: AdminPanel,
meta: {
requiresAuth: true,
roles: ['admin'] // 仅限 admin 角色
}
},
{
path: '/user',
name: 'UserCenter',
component: UserCenter,
meta: {
requiresAuth: true,
roles: ['admin', 'user'] // admin 和 user 都可以看
}
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../views/NotFound.vue'),
meta: { requiresAuth: false }
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
看到那个 meta 了吗?这就是我们的“身份证”。requiresAuth 告诉守卫“这里需要查票”,roles 数组告诉守卫“持有什么票才能进”。
第三步:模拟用户状态管理
为了演示方便,我们不搞复杂的 Vuex 或 Pinia,直接用简单的变量模拟用户状态。在实际项目中,你会从 store 里读取 user.roles 和 token。
// store/userStore.js (简化版)
export const useUserStore = {
state: {
token: localStorage.getItem('token') || '',
roles: [] // 例如: ['admin'] 或 ['user']
},
setToken(token) {
this.token = token;
localStorage.setItem('token', token);
},
setRoles(roles) {
this.roles = roles;
},
logout() {
this.token = '';
this.roles = [];
localStorage.removeItem('token');
}
}
第四步:核心代码——全局前置守卫
这是整篇文章的灵魂。我们将所有的逻辑都放在 router.beforeEach 中。
// router/index.js (接上面的代码)
// 假设我们有一个获取用户角色的辅助函数,实际项目中可能从 API 或 Store 获取
function getUserRoles() {
// 这里简化处理,实际应该从 store 或 cookie 解析 jwt 获取
return JSON.parse(localStorage.getItem('user_roles') || '[]');
}
function getToken() {
return localStorage.getItem('token');
}
router.beforeEach((to, from, next) => {
const token = getToken();
const userRoles = getUserRoles();
// 1. 判断是否需要登录
if (to.meta.requiresAuth) {
// 如果没有 token,说明未登录
if (!token) {
console.log('🚫 未登录,重定向到登录页');
// 可选:记录当前想去的页面,登录后跳回来
next({
path: '/login',
query: { redirect: to.fullPath }
});
return;
}
// 2. 如果有 token,检查角色权限
// 如果路由没有定义 roles 限制(例如 meta.roles 为空或未定义),则默认允许访问
if (to.meta.roles && to.meta.roles.length > 0) {
// 检查用户角色是否在允许的列表中
const hasPermission = userRoles.some(role => to.meta.roles.includes(role));
if (!hasPermission) {
console.log('⛔ 权限不足,重定向到首页或403页面');
// 方案A: 跳转到首页
// next('/');
// 方案B: 跳转到专门的无权限页面(推荐)
next({
path: '/403',
replace: true // 防止用户点击后退回到原页面导致无限循环
});
return;
}
}
} else {
// 3. 处理已登录用户访问登录页的情况
// 如果用户已经登录了,还去访问 /login,直接把他踢回首页
if (to.path === '/login' && token) {
console.log('✅ 已登录,访问登录页,重定向到首页');
next('/');
return;
}
}
// 4. 一切正常,放行
next();
});
深度解析这段代码的逻辑陷阱
很多初学者在这里会踩坑,咱们来拆解一下:
为什么用
some?to.meta.roles是一个数组,比如['admin', 'editor']。用户的角色也可能是多个,比如['admin']。我们需要判断用户的角色是否有“交集”。some方法正好适合这种“只要有一个匹配就通过”的逻辑。为什么
next({ replace: true })? 当用户试图访问/admin但权限不够时,如果我们只是next('/403'),浏览器历史记录里还会保留/admin。如果用户按“后退”键,可能会再次触发守卫,造成困惑或者死循环。使用replace: true可以替换掉当前的历史条目,让用户无法通过后退键回到那个他无权访问的页面。动态获取角色 注意
getUserRoles()函数。在大型应用中,用户角色可能是在登录后通过异步请求获取的。如果守卫执行时角色还没加载完怎么办?- 最佳实践:在登录成功后,立即将角色存入本地存储或 Pinia/Vuex 的状态中。这样守卫读取时就是同步且准确的。
- 进阶技巧:如果角色是异步获取的,可以在
router.beforeEach中判断如果角色未加载,则等待加载完成后再执行后续逻辑,但这会增加复杂度。对于大多数中小项目,登录时存好角色是最稳妥的方案。
第五步:实战演练——登录组件与权限刷新
光有守卫不够,我们还得确保用户登录后,状态能正确更新,并且路由能正确跳转。
登录页 (Login.vue)
<template>
<div class="login-container">
<h2>用户登录</h2>
<button @click="handleLogin('user')">模拟用户登录</button>
<button @click="handleLogin('admin')">模拟管理员登录</button>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router';
const router = useRouter();
const handleLogin = async (roleType) => {
// 模拟 API 请求
try {
// 假设后端返回了 token 和用户信息
const mockResponse = {
token: 'fake-jwt-token-' + roleType,
user: {
id: 1,
name: roleType === 'admin' ? 'Admin User' : 'Normal User',
roles: roleType === 'admin' ? ['admin'] : ['user']
}
};
// 1. 保存 Token
localStorage.setItem('token', mockResponse.token);
// 2. 保存角色信息(用于守卫读取)
localStorage.setItem('user_roles', JSON.stringify(mockResponse.user.roles));
// 3. 更新全局状态(如果你有 Pinia/Vuex,这里也要更新 store)
console.log(`登录成功,角色: ${mockResponse.user.roles}`);
// 4. 重定向
// 检查是否有 redirect 参数
const redirect = router.currentRoute.value.query.redirect;
if (redirect) {
router.replace(redirect); // 跳回之前想去的页面
} else {
router.replace('/'); // 默认去首页
}
} catch (error) {
console.error('登录失败', error);
alert('登录失败,请重试');
}
};
</script>
登出功能
别忘了提供登出按钮,清除状态至关重要。
// 在某个布局组件中
const handleLogout = () => {
localStorage.removeItem('token');
localStorage.removeItem('user_roles');
// 清除 store 中的用户信息
// 跳转到登录页,并带上 redirect 以便登录后返回(虽然刚登出不需要,但保持一致性是好习惯)
router.push({ path: '/login', query: { redirect: router.currentRoute.value.fullPath } });
};
第六步:常见问题与高级优化
1. 路由懒加载与权限检查的冲突
有时候我们会遇到这样的场景:用户点击了一个菜单,路由组件还没有加载完,守卫就执行完了。其实 beforeEach 是在路由切换前执行的,此时组件还未挂载,所以不会影响组件内部的逻辑。但是,如果你需要在组件内部再次检查权限(双重保险),那是可以的,但主要的安全防线必须在路由守卫。
2. 动态添加路由(Dynamic Routing)
有一种更高级的做法是:后端返回给用户一个菜单列表,前端根据这个列表动态生成路由表,然后 router.addRoute()。
- 优点:前端代码中不包含敏感的路由定义,完全由后端控制。
- 缺点:实现复杂,需要处理路由重置、权限刷新等问题。
- 建议:对于中小型项目,静态路由+Meta权限控制足够且稳定。对于大型后台系统,可以考虑动态路由。
3. 用户体验优化:Loading 状态
当用户权限不足被拦截时,直接跳到 403 页面可能会有点突兀。你可以加一个简单的过渡动画,或者在跳转前显示一个“正在验证权限…”的提示。
// 在守卫中加入简单的 loading 提示(如果需要)
if (!hasPermission) {
ElMessage.warning('您没有权限访问该页面'); // 假设用了 Element Plus
next({ path: '/403', replace: true });
return;
}
4. 防止 URL 篡改
记住,前端的所有权限控制都是“软”控制。聪明的用户可以禁用 JavaScript,或者直接在浏览器控制台修改 localStorage。因此,任何涉及敏感数据的 API 请求,后端必须进行二次鉴权! 前端守卫的作用是提升用户体验,防止普通用户误操作或恶意浏览,而不是作为唯一的安全屏障。
总结
咱们今天聊了这么多,核心就三点:
- 利用
meta字段:给路由打标,明确谁需要登录,谁需要什么角色。 - 利用
beforeEach守卫:在每次跳转前进行“安检”,检查 Token 和 Role。 - 做好状态管理:登录、登出、权限变更时,确保本地存储和路由状态同步。
这套组合拳打下来,你的 Vue 应用权限控制就已经相当稳固了。代码写得再漂亮,不如跑起来看看效果。建议你新建一个 Vue 3 项目,把上面的代码复制进去,分别用 admin 和 user 账号登录测试一下跳转逻辑,你会发现,原来权限控制也没那么神秘嘛。
如果在这个过程中遇到了什么奇怪的问题,比如“为什么我改了 localStorage 守卫没生效?”(可能是路由实例没更新或者缓存问题),随时回来找我。祝你编码愉快,写出既安全又优雅的代码!
