Vue 权限管理实现方案
实现 Vue 应用的路由、菜单、按钮级别权限控制
问题
在 Vue 应用中如何实现完整的权限管理系统,包括路由权限、菜单权限和按钮级别的权限控制?
解答
权限控制的本质
前端权限控制的核心是控制请求的发起权。请求触发来源有两种:页面加载触发和按钮点击触发。因此需要从路由和视图两方面入手控制。
接口权限
使用 JWT 进行接口权限验证,通过 axios 拦截器在每次请求时携带 token:
axios.interceptors.request.use(config => {
const token = getToken()
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
}, error => {
return Promise.reject(error)
})
axios.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
// 跳转到登录页
router.push('/login')
}
return Promise.reject(error)
}
)
路由权限
方案一:初始化挂载全部路由
在路由配置中标记权限信息,每次跳转前校验:
const routerMap = [
{
path: '/permission',
component: Layout,
redirect: '/permission/index',
alwaysShow: true,
meta: {
title: 'permission',
icon: 'lock',
roles: ['admin', 'editor']
},
children: [...]
}
]
router.beforeEach((to, from, next) => {
const userRoles = store.getters.roles
if (hasPermission(userRoles, to.meta.roles)) {
next()
} else {
next('/401')
}
})
缺点:加载所有路由影响性能,菜单信息写死在前端,路由与菜单耦合。
方案二:动态挂载路由
登录后根据权限动态添加路由:
router.beforeEach(async (to, from, next) => {
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' })
} else {
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 获取用户权限信息
const { roles } = await store.dispatch('user/getInfo')
// 根据权限生成可访问路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 动态添加路由
router.addRoutes(accessRoutes)
next({ ...to, replace: true })
} catch (error) {
await store.dispatch('user/resetToken')
next(`/login?redirect=${to.path}`)
}
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next(`/login?redirect=${to.path}`)
}
}
})
菜单权限
方案一:菜单与路由分离
前端定义路由,后端返回菜单,通过 name 字段关联:
// 前端路由定义
{
name: "userManage",
path: "/user/manage",
component: () => import("@/pages/UserManage.vue")
}
// 后端返回菜单
[
{
name: "userManage",
title: "用户管理",
icon: "user"
}
]
// 权限判断
function hasPermission(router, accessMenu) {
if (whiteList.indexOf(router.path) !== -1) {
return true
}
let menu = accessMenu.find(item => item.name === router.name)
return !!menu
}
方案二:菜单和路由都由后端返回
前端定义路由组件映射表:
// 组件映射
const componentMap = {
home: () => import("@/pages/Home.vue"),
userInfo: () => import("@/pages/UserInfo.vue")
}
// 后端返回
[
{
name: "home",
path: "/",
component: "home"
}
]
// 处理路由
function formatRoutes(routes) {
return routes.map(route => {
return {
...route,
component: componentMap[route.component]
}
})
}
const accessRoutes = formatRoutes(backendRoutes)
router.addRoutes(accessRoutes)
按钮权限
使用自定义指令实现按钮级别权限控制:
// 路由配置
{
path: '/permission',
component: Layout,
meta: {
btnPermissions: ['edit', 'delete', 'add']
}
}
// 自定义指令
import Vue from 'vue'
Vue.directive('has', {
bind: function (el, binding, vnode) {
let btnPermissionsArr = []
if (binding.value) {
// 使用指令传值
btnPermissionsArr = Array.of(binding.value)
} else {
// 使用路由配置
btnPermissionsArr = vnode.context.$route.meta.btnPermissions
}
if (!Vue.prototype.$_has(btnPermissionsArr)) {
el.parentNode.removeChild(el)
}
}
})
// 权限检查方法
Vue.prototype.$_has = function (value) {
const btnPermissionsStr = sessionStorage.getItem("btnPermissions")
if (!btnPermissionsStr) {
return false
}
return value.some(item => btnPermissionsStr.includes(item))
}
使用方式:
<template>
<div>
<el-button @click="editClick" type="primary" v-has="'edit'">编辑</el-button>
<el-button @click="deleteClick" type="danger" v-has="'delete'">删除</el-button>
</div>
</template>
关键点
- 接口权限通过 JWT 和 axios 拦截器实现,401 状态码跳转登录页
- 路由权限推荐动态挂载方案,登录后根据用户权限动态添加路由,避免加载无权限路由
- 菜单权限可选择菜单与路由分离或完全由后端返回,根据项目复杂度选择合适方案
- 按钮权限使用自定义指令 v-has 实现,通过移除 DOM 元素控制按钮显示
- 前端权限控制是辅助手段,核心权限验证必须在后端完成
目录