VUE项目中实现权限控制,菜单权限,按钮权限,接口权限,路由权限,操作权限,数据权限实现
VUE项目中实现权限控制,菜单权限,按钮权限,接口权限,路由权限,操作权限,数据权限实现
- 权限系统分类(RBAC)
- 引言
- 菜单权限
- 按钮权限
- 接口权限
- 路由权限
- 菜单权限方案
- 方案一:菜单与路由分离
- 方案二:菜单和路由都由后端返回
- 路由权限控制(如果后端设计菜单后返回的菜单都挂载到路由上)
- 方案一:初始化即挂载全部路由
- 方案二:按需挂载路由
- 按钮权限方案
- 方案一:使用 `v-if` 判断
- 方案二:通过自定义指令进行按钮权限判断
权限系统分类(RBAC)
引言
在实际项目开发中,权限管理是一个关键功能,用于控制不同用户对系统资源的访问。权限是对特定资源的访问许可,权限控制的目的是确保用户只能访问到被分配的资源。例如,网站管理员可以对网站数据进行增删改查,而普通用户只能浏览
菜单权限
菜单管理涉及定义和管理应用中的导航菜单。不同的用户角色可能会看到不同的菜单项,从而访问不同的功能模块。
按钮权限
不同的用户角色可能享有不同的操作权限,例如管理员可以增删改查,而普通用户只能查看。
接口权限
接口权限通常采用 JWT 形式进行验证。如果请求未通过验证,服务器会返回 401 状态码,客户端则跳转到登录页面重新登录。登录成功后,客户端会拿到 token 并将其存储起来,通过 axios 请求拦截器在每次请求时携带 token。
路由权限
路由权限控制用户可以访问的页面和路径。
菜单权限方案
方案一:菜单与路由分离
前端组件:
{
name: "login",
path: "/login",
component: () => import("@/pages/Login.vue")
}
全局路由卫生
function hasPermission(router, accessMenu) {
if (whiteList.includes(router.path)) {
return true;
}
const menu = Util.getMenuByName(router.name, accessMenu);
return !!menu.name;
}
Router.beforeEach(async (to, from, next) => {
if (getToken()) {
const userInfo = store.state.user.userInfo;
if (!userInfo.name) {
try {
await store.dispatch("GetUserInfo"); // 获取用户信息
await store.dispatch('updateAccessMenu'); // 更新访问菜单
if (to.path === '/login') {
next({ name: 'home_index' }); // 如果当前路径是登录页,跳转到首页
} else {
next({ ...to, replace: true }); // 替换当前路径
}
} catch (e) {
if (whiteList.includes(to.path)) {
next(); // 如果路径在白名单中,直接通过
} else {
next('/login'); // 否则跳转到登录页
}
}
} else {
if (to.path === '/login') {
next({ name: 'home_index' }); // 如果当前路径是登录页,跳转到首页
} else {
if (hasPermission(to, store.getters.accessMenu)) {
Util.toDefaultPage(store.getters.accessMenu, to, routes, next); // 跳转到默认页面
} else {
next({ path: '/403', replace: true }); // 没有权限,跳转到403页面
}
}
}
} else {
if (whiteList.includes(to.path)) {
next(); // 如果路径在白名单中,直接通过
} else {
next('/login'); // 否则跳转到登录页
}
}
const menu = Util.getMenuByName(to.name, store.getters.accessMenu);
Util.title(menu.title); // 设置页面标题
});
Router.afterEach((to) => {
window.scrollTo(0, 0); // 滚动条回到顶部
});
优点:
前后端职责分明,前端负责路由定义,后端负责菜单管理。
缺点:
需要维护菜单与路由的对应关系,增加了复杂性。
每次路由跳转都需要进行权限判断
不推荐!!!!!
方案二:菜单和路由都由后端返回
思路: 由后端设计菜单表进行菜单管理,返回给前端后,由前端进行拼接成路由,然后进行挂载到全局路由上,router.addRouter()方法上。进行加载,
后端返回菜单json格式
[
{
name: "home",
path: "/",
component: "home"
},
{
name: "userinfo",
path: "/userinfo",
component: "userInfo"
}
]
前端处理逻辑
// 将后端菜单数据转换为路由格式
function generateAsyncRouter(menus) {
const asyncRoutes = menus.map(menu => {
const route = {
path: menu.path,
name: menu.menuName,
meta: {
title: menu.menuName,
icon: menu.icon
},
hidden: menu.hidden
}
// 处理组件
if (menu.menuType === 'content') {
// 目录类型
route.alwaysShow = true
route.component = Layout
if (menu.children && menu.children.length > 0) {
route.children = generateAsyncRouter(menu.children)
}
} else if (menu.menuType === 'menu') {
// 菜单类型,动态加载组件
route.component = loadView(menu.path)
}
// button类型的不需要生成路由
return route
})
// 添加404页面路由
asyncRoutes.push({
path: '*',
redirect: '/404',
hidden: true
})
优点:
前后端高度集成,灵活性高。
缺点:
前后端配合要求更高。
每次路由跳转都需要进行权限判断。
推荐这种用法
路由权限控制(如果后端设计菜单后返回的菜单都挂载到路由上)
方案一:初始化即挂载全部路由
const routerMap = [
{
path: '/permission',
component: Layout,
redirect: '/permission/index',
alwaysShow: true,
meta: {
title: '权限管理',
icon: 'lock',
roles: ['admin', 'editor'] // 标记权限
},
children: [
{
path: 'page',
component: () => import('@/views/permission/page'),
name: 'pagePermission',
meta: {
title: '页面权限',
roles: ['admin'] // 标记权限
}
},
{
path: 'directive',
component: () => import('@/views/permission/directive'),
name: 'directivePermission',
meta: {
title: '指令权限'
// 如果没有设置权限标识,意味着这个页面不需要权限
}
}
]
}
];
方案二:按需挂载路由
import router from './router';
import store from './store';
import { Message } from 'element-ui';
import NProgress from 'nprogress'; // 进度条
import 'nprogress/nprogress.css'; // 进度条样式
import { getToken } from '@/utils/auth'; // 从 cookie 获取 token
NProgress.configure({ showSpinner: false }); // 配置进度条
function hasPermission(roles, permissionRoles) {
if (roles.includes('admin')) return true; // 管理员权限直接通过
if (!permissionRoles) return true;
return roles.some(role => permissionRoles.includes(role));
}
const whiteList = ['/login', '/authredirect']; // 不需要重定向的白名单
router.beforeEach((to, from, next) => {
NProgress.start(); // 开始进度条
if (getToken()) { // 确定是否有 token
if (to.path === '/login') {
next({ path: '/' });
NProgress.done(); // 如果当前页面是仪表盘,不会触发 afterEach 钩子,所以手动处理
} else {
if (store.getters.roles.length === 0) { // 用户信息
store.dispatch('GetUserInfo').then(res => { // 获取用户信息
const roles = res.data.roles; // 注意:角色必须是一个数组!例如:['editor','develop']
store.dispatch('GenerateRoutes', { roles }).then(() => { // 生成路由
router.addRoutes(store.getters.addRouters); // 添加路由
next({ ...to, replace: true }); // 替换当前路径,防止留下历史记录
});
}).catch((err) => {
store.dispatch('FedLogOut').then(() => {
Message.error(err || '验证失败,请重新登录');
next({ path: '/' });
});
});
} else {
if (hasPermission(store.getters.roles, to.meta.roles)) {
next(); // 有权限,继续跳转
} else {
next({ path: '/401', replace: true, query: { noGoBack: true }}); // 没有权限,跳转到401页面
}
}
}
} else {
if (whiteList.includes(to.path)) {
next(); // 如果路径在白名单中,直接通过
} else {
next('/login'); // 否则跳转到登录页
NProgress.done(); // 如果当前页面是登录页,不会触发 afterEach 钩子,所以手动处理
}
}
});
router.afterEach(() => {
NProgress.done(); // 结束进度条
});
这种是我实现的方式
全局路由上,获取到全部菜单,前端进行路由加载
按钮权限方案
方案一:使用 v-if
判断
优点:
简单直观,易于实现。
缺点:
如果页面较多,每个页面都需要获取用户权限并进行判断,增加复杂性。
方案二:通过自定义指令进行按钮权限判断
自定义指令:
import permissionV from './permission-v';
const install = function(Vue) {
Vue.directive('permission-v', permissionV)
}
if (window.Vue) {
window['permission-v'] = permissionV
Vue.use(install); // eslint-disable-line
}
permissionV.install = install
export default permissionV
permission-v.js
import store from '@/store'
function checkBtnPermission(el, binding) {
const { value } = binding
const permissions = store.getters && store.getters.userInfo.permissions
if (value && value instanceof Array) {
if (value.length > 0) {
const permission = value
const hasPermission = permissions.some(permiss => {
return permission.includes(permiss)
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
}
} else {
throw new Error(`need permissions! Like v-permission="['admin','editor']"`)
}
}
export default {
inserted(el, binding) {
checkBtnPermission(el, binding)
},
update(el, binding) {
checkBtnPermission(el, binding)
}
}
注册到全局
import permission from '@/directive/permission-v'
// 注册全局指令
Vue.use(permission)
Vue.directive('permission-v', permission)
//使用方式 v-permission-v
<el-button v-permission-v="['system:role:add']" type="primary" @click="handleAdd" icon="el-icon-plus">新增角色</el-button>
感兴趣的同学可以关注下,日常更新技术文章和demo示例免费的。
https://bytecodestudio.site/