(五)Vue3 管理后台:权限控制原理 + 完整流程梳理
对应我们项目:登录 → 获取用户信息 + 角色权限 → 路由守卫 → 按钮级权限控制 (v‑perm 指令)
一、整体业务流程总览
用户输入账号密码登录
↓
1.登录接口获取token,存入localStorage,pinia保存token
↓
2.跳转首页 /home,触发路由守卫 router.beforeEach
↓
3.路由守卫判断:本地是否有token
├─无token → 强制跳转到登录页
└─有token → 调用 /api/getUserInfo 获取用户信息{username, roles:['user:add','user:edit']},存入pinia userStore
↓
4.页面渲染,全局自定义指令 v‑perm 执行
↓
5.v‑perm读取当前用户roles,对比按钮绑定的权限标识
├─拥有权限:保留DOM按钮
└─没有权限:直接移除DOM元素,按钮不显示
↓
用户退出登录:清空token、清空pinia用户信息,跳转登录页
二、分模块拆解原理
1. Token 作用(身份凭证)
- 用户登录成功后端返回
token,保存localStorage,持久化,刷新页面不会丢失。 - 后续每次接口请求,axios 请求拦截器自动把 token 放到请求头
Authorization,后端靠 token 识别是谁在访问。 - token 过期,响应拦截捕获 401,清空本地信息,退回登录页。
token 只代表 "你是谁";不代表你有什么权限 ;权限来自
getUserInfo返回的roles数组。
2. Pinia userStore:存放用户数据
src/stores/user.js
- state:
token、userInfo:{id, username, roles:[]} - loginAction:保存 token
- getUserInfoAction:调用接口,把后端返回的
roles存入userInfo - logoutAction:清空 token、清空 userInfo,清除 localStorage
⚠️重点坑点:
pinia 必须在 Vue 组件实例环境下执行;如果在 main.js 全局指令钩子错误时机调用 useUserStore,拿不到真实用户数据,只能拿到初始默认空对象 → 权限失效,也就是刚才遇到的 bug 。
✅正确:
mounted / updated钩子内部再执行useUserStore()。
3. 路由守卫 router/index.js beforeEach
路由跳转前执行,做页面访问权限拦截。
router.beforeEach((to, from, next)=>{
const token = localStorage.getItem('token')
// 没token,非登录页面,跳登录
if(!token && to.path !== '/login') return next('/login')
// 有token访问登录页,直接跳首页
if(token && to.path === '/login') return next('/home')
// 有token,但是还没有拉取用户信息,则调用store.getUserInfo()
if(token && !userStore.userInfo.username){
await userStore.getUserInfo()
}
next()
})
路由守卫控制页面能不能进 ;v‑perm 控制页面内部按钮显隐。
4. 按钮权限自定义指令 v‑perm 原理
文件
main.js注册全局指令
app.directive('perm',{
mounted(el, binding){
// el:绑定指令的DOM元素(就是按钮)
// binding.value:模板传过来的值 v‑perm="'user:add'" → binding.value = 'user:add'
const permission = binding.value
const userStore = useUserStore()
const roles = userStore.userInfo.roles
// hasPermission工具函数判断:用户roles数组是否包含当前按钮需要的权限标识
if(!hasPermission(permission, roles)){
// 没有权限,直接把按钮DOM从父节点删除,页面看不到
el.parentNode.removeChild(el)
}
},
updated() {
// 数据更新(切换角色)时重新执行权限校验
}
})
utils/permission.js
export function hasPermission(permission, roles) {
if (!permission) return true //没写权限标识默认放行
return roles.includes(permission)
}
模板使用:
<el-button v-perm="'user:add'">新增用户</el-button>
对比
v‑if:
v‑if:Vue 虚拟 DOM 层面控制,dom 可能还留在内存。- 自定义指令 v‑perm:直接物理删除真实 DOM 节点,调试工具看不到这个按钮,相对安全一点。
5.Mock 模拟接口角色数据
src/mock/user.js /api/getUserInfo返回 roles 数组
data:{
username:'admin',
roles:['user:add','user:edit','user:delete']
}
roles 数组是权限的数据源,所有权限判断全部基于这个数组。
修改 mock 后必须重启 pnpm dev,mock 才生效;修改完要退出登录重新登录,重新拉取用户信息。
6. 退出登录流程
- 调用
userStore.logout() - 清空 pinia 中 token、userInfo
- localStorage 清除 token
- router 跳转到登录页
三、区分:页面权限 vs 按钮权限
表格
| 类型 | 实现方式 | 作用场景 |
|---|---|---|
| 页面访问权限 | 路由守卫 beforeEach | 控制用户能不能打开某个路由页面 |
| 按钮级权限 | 自定义指令 v‑perm /v‑if | 同一个页面,不同角色看不同按钮,新增 / 编辑 / 删除隐藏 |
拓展:后续做动态路由 ,就是后端返回菜单列表,前端根据 roles 过滤菜单,调用
router.addRoute()动态生成侧边栏菜单,控制哪些菜单显示。
四、刚才踩坑复盘(权限失效)
❌错误写法:在指令外面提前实例化const userStore = useUserStore()
// 错误!全局顶层执行,此时pinia还没挂载,拿不到登录后的用户数据
const userStore = useUserStore()
app.directive('perm',{
mounted(el){
const roles = userStore.userInfo.roles
}
})
✅正确写法:进入 mounted 钩子函数内部,再调用 useUserStore (),此时 vue、pinia 实例已经完成挂载,可以拿到登录后的真实状态。
五、完整数据流简图
Mock后端返回roles数组
↓
axios请求getUserInfo
↓
pinia userStore.userInfo.roles 保存
↓
├─路由守卫:根据roles做页面控制(动态路由用)
└─v‑perm指令读取roles数组,和按钮标识对比,移除无权限DOM