权限管理
权限控制是中后台系统中常见的需求之一,你可以利用我们提供的 路由权限 和 指令权限,实现一些基本的权限控制功能。
路由和默认权限控制
项目提供了两套权限实现方案,其中默认方案为前端固定路由表和权限配置,由后端提供用户权限标识,来识别是否拥有该路由权限。另一套方案为后端提供权限和路由信息结构接口,动态生成权限和菜单。
默认实现方式是通过获取当前用户的权限去比对路由表,生成当前用户具有的权限可访问的路由表,通过 router.addRoutes 动态挂载到 router 上。
整体流程可以看这张图:
步骤如下:
- 判断是否有 AccessToken 如果没有则跳转到登录页面
- 获取用户信息和拥有权限store.dispatch(‘GetInfo’)
- 用户信息获取成功后, 调用 store.dispatch(‘GenerateRoutes’, userInfo) 根据获取到的用户信息构建出一个已经过滤好权限的路由结构(src/store/modules/permission.js)
- 将构建的路由结构信息利用 Vue-Router 提供的动态增加路由方法 router.addRoutes 加入到路由表中
- 加入路由表后将页面跳转到用户原始要访问的页面,如果没有 redirect 则进入默认页面 (/dashboard/workplace)
这里可以看出把 登录 和 获取用户信息 分成了两个接口,主要目的在于当用户刷新页面时,可以根据登录时获取到的身份令牌(cookie/token)等,去获取用户信息,从而避免刷新需要调用登录接口
实现的路由权限的控制代码都在 @/permission.js 中,如果想修改逻辑,直接在适当的判断逻辑中 next() 释放钩子即可。
两套权限实现 均使用 @/permission.js (路由守卫)来进行控制。
动态路由
但其实很多公司的业务逻辑可能并不是上面描述的简单实现方案,比如正常业务逻辑下 每个页面的信息都是动态从后端配置的,并不是像 默认的路由表那样写死在预设的。你可以在后台通过一个 tree 或者其它展现形式给每一个页面动态配置权限,之后将这份路由表存储到后端。
权限/菜单

权限/功能
角色/权限

由 角色关联 到多个 权限(菜单) 。 角色 1 对多权限,用户 1 对多角色 或 用户 1 对 1 角色;
当用户登录后得到 roles,前端根据 roles 去向后端请求可访问的路由表,从而动态生成可访问页面,之后就是 router.addRoutes 动态挂载到 router 上,你会发现原来是相同的,万变不离其宗。
权限菜单功能实现
菜单权限的控制
重点:路由动态的权限是由:permission: [‘system’]来控制的,system可理解为角色id,如果用户拥有多个角色id可以写成:permission: [‘system’,’admin’,…]
这里我是通过角色id进行控制的,菜单查询-查询全量,后通过设置拥有的角色进行控制菜单的权限。
菜单动态获取及实现
后端获取菜单数据
// 后端返回的 JSON 动态路由结构const servicePermissionMap = {"message": "","result": [{"title": "首页","key": "","name": "index","component": "BasicLayout","redirect": "/dashboard/workplace","children": [{"title": "仪表盘","key": "dashboard","component": "RouteView","icon": "dashboard","children": [{"title": "分析页","key": "analysis","icon": ""},...]},{"title": "系统管理","key": "system","component": "PageView","icon": "setting","children": [{"title": "用户管理","key": "userList"},...]}]}],"status": 200,"timestamp": 1534844188679}
import { asyncRouterMap, constantRouterMap } from '@/config/router.config'import { getTreeList } from "@/api/system/menu";import { BasicLayout, UserLayout } from '@/layouts'const RouteView = {name: 'RouteView',render: (h) => h('router-view'),}/*** 过滤账户是否拥有某一个权限,并将菜单从加载列表移除** @param permission* @param route* @returns {boolean}*/function hasPermission(permission, route) {if (route.meta && route.meta.permission) {let flag = falsefor (let i = 0, len = permission.length; i < len; i++) {flag = route.meta.permission.includes(permission[i])if (flag) {return true}}return false}return false}/*** 单账户多角色时,使用该方法可过滤角色不存在的菜单** @param roles* @param route* @returns {*}*/// eslint-disable-next-linefunction hasRole(roles, route) {if (route.meta && route.meta.roles) {return route.meta.roles.includes(roles.id)} else {return true}}function filterAsyncRouter(routerMap, roles) {const accessedRouters = routerMap.filter(route => {if (hasPermission(roles.permissionList, route)) {if (route.children && route.children.length) {route.children = filterAsyncRouter(route.children, roles)}return true}return false})return accessedRouters}function filterEmptyDirectory(routerMap) {const accessedRouters = routerMap.filter(route => {if (route.children && route.children.length) {route.children = filterEmptyDirectory(route.children)return true} else {if (route.meta.permission.includes('system')) {return false} else {return true}}})return accessedRouters}function fommat({arrayList,pidStr = "parent_id",idStr = "id",childrenStr = "children",}) {arrayList.push({path: "/",name: "index",component: BasicLayout,title: '主页',redirect: '/dashboard/welcome',id: "1",child_num: 1});let listOjb = {}; // 用来储存{key: obj}格式的对象let treeList = []; // 用来储存最终树形结构数据的数组// 将数据变换成{key: obj}格式,方便下面处理数据for (let i = 0; i < arrayList.length; i++) {var data = arrayList[i];data.key = data.id;data.icon = "";//处理菜单格式信息if (data.type == '0') {//目录data.component = RouteView} else if (data.type == '1') {const views = data.component;if (views == '/dashboard/Welcome' || views == '/dashboard/welcome') {data.component = (resolve) => require([`@/views/dashboard/Welcome`], resolve)} else {data.component = (resolve) => require([`@/views${views}/Index`], resolve)}// data.component = () => import(`@${views}`)// data.component = () => import('@/views${component}')// data.component = this.loadView(component)} else if (data.type == '4') {data.component = (resolve) => require([`@/views/system/Iframe/Index`], resolve)}const role_ids = data.role_ids?.split(',')if (data.child_num > 0) {if (role_ids == undefined) {role_ids = ['system']} else {role_ids.push('system');}}if (data.type == '3') {data.meta = {title: data.title,keepAlive: data.keepAlive,icon: data.qf_icon,permission: role_ids,target: '_black'}} else {data.meta = {title: data.title,keepAlive: data.keepAlive,icon: data.qf_icon,permission: role_ids}}// console.log(data.meta)listOjb[arrayList[i][idStr]] = data;}// 根据pid来将数据进行格式化for (let j = 0; j < arrayList.length; j++) {// 判断父级是否存在let haveParent = listOjb[arrayList[j][pidStr]];if (haveParent) {// 如果有没有父级children字段,就创建一个children字段!haveParent[childrenStr] && (haveParent[childrenStr] = []);// 在父级里插入子项haveParent[childrenStr].push(arrayList[j]);} else {// 如果没有父级直接插入到最外层treeList.push(arrayList[j]);}}return treeList;}// function loadView (view) {// return () => import(`@/views/${view}`)// // return (resolve) => require([`@/views/${view}`], resolve)// }const permission = {state: {routers: constantRouterMap,addRouters: [],},mutations: {SET_ROUTERS: (state, routers) => {state.addRouters = routersstate.routers = constantRouterMap.concat(routers)},},actions: {GenerateRoutes({ commit }, data) {return new Promise(resolve => {const { roles } = datagetTreeList({ types: '0,1,3,4' }).then((response) => {const treeData = fommat({arrayList: response.data.data,pidStr: "parent_id",});const accessedRouters = filterAsyncRouter(treeData, roles)const finalRouters = filterEmptyDirectory(accessedRouters)commit('SET_ROUTERS', finalRouters)resolve()});})},},}export default permission
指令权限
封装了一个非常方便实现按钮级别权限的自定义指令。
使用案例:
<template><!-- 校验是否有 dashboard 权限下的 add 操作权限 --><a-button v-action:add >添加用户</a-button><!-- 校验是否有 dashboard 权限下的 del 操作权限 --><a-button v-action:del>删除用户</a-button><!-- 校验是否有 dashboard 权限下的 edit 操作权限 --><a v-action:edit @click="edit(record)">修改</a></template>
需要注意的是,指令权限默认从 store 中获取当前已经登陆的用户的角色和权限信息进行比对,所以也要对指令权限的获取和校验 Action 权限部分进行自定义。
在某些情况下,不适合使用 v-action,例如 Tab 组件,只能通过手动设置 v-if 来实现。
这时候,为其提供了原始 v-if 级别的权限判断。
<template><a-tabs><a-tab-pane v-if="$auth('dashboard.add')" tab="Tab 1">some context..</a-tab-pane><a-tab-pane v-if="$auth('dashboard.del')" tab="Tab 2">some context..</a-tab-pane><a-tab-pane v-if="$auth('dashboard.edit')" tab="Tab 3">some context..</a-tab-pane></a-tabs></template>
以上代码的 if 判断会检查,当前登录用户是否存在 dashboard 下的 add / del / edit 权限
