权限管理

权限控制是中后台系统中常见的需求之一,你可以利用我们提供的 路由权限指令权限,实现一些基本的权限控制功能。

路由和默认权限控制

项目提供了两套权限实现方案,其中默认方案为前端固定路由表和权限配置,由后端提供用户权限标识,来识别是否拥有该路由权限。另一套方案为后端提供权限和路由信息结构接口,动态生成权限和菜单。
默认实现方式是通过获取当前用户的权限去比对路由表,生成当前用户具有的权限可访问的路由表,通过 router.addRoutes 动态挂载到 router 上。
整体流程可以看这张图:
动态路由和权限控制 - 图1
步骤如下:

  1. 判断是否有 AccessToken 如果没有则跳转到登录页面
  2. 获取用户信息和拥有权限store.dispatch(‘GetInfo’)
  3. 用户信息获取成功后, 调用 store.dispatch(‘GenerateRoutes’, userInfo) 根据获取到的用户信息构建出一个已经过滤好权限的路由结构(src/store/modules/permission.js)
  4. 将构建的路由结构信息利用 Vue-Router 提供的动态增加路由方法 router.addRoutes 加入到路由表中
  5. 加入路由表后将页面跳转到用户原始要访问的页面,如果没有 redirect 则进入默认页面 (/dashboard/workplace)

这里可以看出把 登录 和 获取用户信息 分成了两个接口,主要目的在于当用户刷新页面时,可以根据登录时获取到的身份令牌(cookie/token)等,去获取用户信息,从而避免刷新需要调用登录接口

实现的路由权限的控制代码都在 @/permission.js 中,如果想修改逻辑,直接在适当的判断逻辑中 next() 释放钩子即可。
两套权限实现 均使用 @/permission.js (路由守卫)来进行控制。

动态路由

但其实很多公司的业务逻辑可能并不是上面描述的简单实现方案,比如正常业务逻辑下 每个页面的信息都是动态从后端配置的,并不是像 默认的路由表那样写死在预设的。你可以在后台通过一个 tree 或者其它展现形式给每一个页面动态配置权限,之后将这份路由表存储到后端。

权限/菜单

image.png

权限/功能

image.png

角色/权限

image.png
由 角色关联 到多个 权限(菜单) 。 角色 1 对多权限用户 1 对多角色用户 1 对 1 角色
当用户登录后得到 roles,前端根据 roles 去向后端请求可访问的路由表,从而动态生成可访问页面,之后就是 router.addRoutes 动态挂载到 router 上,你会发现原来是相同的,万变不离其宗。

权限菜单功能实现

菜单权限的控制

重点:路由动态的权限是由:permission: [‘system’]来控制的,system可理解为角色id,如果用户拥有多个角色id可以写成:permission: [‘system’,’admin’,…]
这里我是通过角色id进行控制的,菜单查询-查询全量,后通过设置拥有的角色进行控制菜单的权限。
image.png
image.png

菜单动态获取及实现

后端获取菜单数据

  1. // 后端返回的 JSON 动态路由结构
  2. const servicePermissionMap = {
  3. "message": "",
  4. "result": [
  5. {
  6. "title": "首页",
  7. "key": "",
  8. "name": "index",
  9. "component": "BasicLayout",
  10. "redirect": "/dashboard/workplace",
  11. "children": [
  12. {
  13. "title": "仪表盘",
  14. "key": "dashboard",
  15. "component": "RouteView",
  16. "icon": "dashboard",
  17. "children": [
  18. {
  19. "title": "分析页",
  20. "key": "analysis",
  21. "icon": ""
  22. },
  23. ...
  24. ]
  25. },
  26. {
  27. "title": "系统管理",
  28. "key": "system",
  29. "component": "PageView",
  30. "icon": "setting",
  31. "children": [
  32. {
  33. "title": "用户管理",
  34. "key": "userList"
  35. },
  36. ...
  37. ]
  38. }
  39. ]
  40. }
  41. ],
  42. "status": 200,
  43. "timestamp": 1534844188679
  44. }
  1. import { asyncRouterMap, constantRouterMap } from '@/config/router.config'
  2. import { getTreeList } from "@/api/system/menu";
  3. import { BasicLayout, UserLayout } from '@/layouts'
  4. const RouteView = {
  5. name: 'RouteView',
  6. render: (h) => h('router-view'),
  7. }
  8. /**
  9. * 过滤账户是否拥有某一个权限,并将菜单从加载列表移除
  10. *
  11. * @param permission
  12. * @param route
  13. * @returns {boolean}
  14. */
  15. function hasPermission(permission, route) {
  16. if (route.meta && route.meta.permission) {
  17. let flag = false
  18. for (let i = 0, len = permission.length; i < len; i++) {
  19. flag = route.meta.permission.includes(permission[i])
  20. if (flag) {
  21. return true
  22. }
  23. }
  24. return false
  25. }
  26. return false
  27. }
  28. /**
  29. * 单账户多角色时,使用该方法可过滤角色不存在的菜单
  30. *
  31. * @param roles
  32. * @param route
  33. * @returns {*}
  34. */
  35. // eslint-disable-next-line
  36. function hasRole(roles, route) {
  37. if (route.meta && route.meta.roles) {
  38. return route.meta.roles.includes(roles.id)
  39. } else {
  40. return true
  41. }
  42. }
  43. function filterAsyncRouter(routerMap, roles) {
  44. const accessedRouters = routerMap.filter(route => {
  45. if (hasPermission(roles.permissionList, route)) {
  46. if (route.children && route.children.length) {
  47. route.children = filterAsyncRouter(route.children, roles)
  48. }
  49. return true
  50. }
  51. return false
  52. })
  53. return accessedRouters
  54. }
  55. function filterEmptyDirectory(routerMap) {
  56. const accessedRouters = routerMap.filter(route => {
  57. if (route.children && route.children.length) {
  58. route.children = filterEmptyDirectory(route.children)
  59. return true
  60. } else {
  61. if (route.meta.permission.includes('system')) {
  62. return false
  63. } else {
  64. return true
  65. }
  66. }
  67. })
  68. return accessedRouters
  69. }
  70. function fommat({
  71. arrayList,
  72. pidStr = "parent_id",
  73. idStr = "id",
  74. childrenStr = "children",
  75. }) {
  76. arrayList.push({
  77. path: "/",
  78. name: "index",
  79. component: BasicLayout,
  80. title: '主页',
  81. redirect: '/dashboard/welcome',
  82. id: "1",
  83. child_num: 1
  84. });
  85. let listOjb = {}; // 用来储存{key: obj}格式的对象
  86. let treeList = []; // 用来储存最终树形结构数据的数组
  87. // 将数据变换成{key: obj}格式,方便下面处理数据
  88. for (let i = 0; i < arrayList.length; i++) {
  89. var data = arrayList[i];
  90. data.key = data.id;
  91. data.icon = "";
  92. //处理菜单格式信息
  93. if (data.type == '0') {//目录
  94. data.component = RouteView
  95. } else if (data.type == '1') {
  96. const views = data.component;
  97. if (views == '/dashboard/Welcome' || views == '/dashboard/welcome') {
  98. data.component = (resolve) => require([`@/views/dashboard/Welcome`], resolve)
  99. } else {
  100. data.component = (resolve) => require([`@/views${views}/Index`], resolve)
  101. }
  102. // data.component = () => import(`@${views}`)
  103. // data.component = () => import('@/views${component}')
  104. // data.component = this.loadView(component)
  105. } else if (data.type == '4') {
  106. data.component = (resolve) => require([`@/views/system/Iframe/Index`], resolve)
  107. }
  108. const role_ids = data.role_ids?.split(',')
  109. if (data.child_num > 0) {
  110. if (role_ids == undefined) {
  111. role_ids = ['system']
  112. } else {
  113. role_ids.push('system');
  114. }
  115. }
  116. if (data.type == '3') {
  117. data.meta = {
  118. title: data.title,
  119. keepAlive: data.keepAlive,
  120. icon: data.qf_icon,
  121. permission: role_ids,
  122. target: '_black'
  123. }
  124. } else {
  125. data.meta = {
  126. title: data.title,
  127. keepAlive: data.keepAlive,
  128. icon: data.qf_icon,
  129. permission: role_ids
  130. }
  131. }
  132. // console.log(data.meta)
  133. listOjb[arrayList[i][idStr]] = data;
  134. }
  135. // 根据pid来将数据进行格式化
  136. for (let j = 0; j < arrayList.length; j++) {
  137. // 判断父级是否存在
  138. let haveParent = listOjb[arrayList[j][pidStr]];
  139. if (haveParent) {
  140. // 如果有没有父级children字段,就创建一个children字段
  141. !haveParent[childrenStr] && (haveParent[childrenStr] = []);
  142. // 在父级里插入子项
  143. haveParent[childrenStr].push(arrayList[j]);
  144. } else {
  145. // 如果没有父级直接插入到最外层
  146. treeList.push(arrayList[j]);
  147. }
  148. }
  149. return treeList;
  150. }
  151. // function loadView (view) {
  152. // return () => import(`@/views/${view}`)
  153. // // return (resolve) => require([`@/views/${view}`], resolve)
  154. // }
  155. const permission = {
  156. state: {
  157. routers: constantRouterMap,
  158. addRouters: [],
  159. },
  160. mutations: {
  161. SET_ROUTERS: (state, routers) => {
  162. state.addRouters = routers
  163. state.routers = constantRouterMap.concat(routers)
  164. },
  165. },
  166. actions: {
  167. GenerateRoutes({ commit }, data) {
  168. return new Promise(resolve => {
  169. const { roles } = data
  170. getTreeList({ types: '0,1,3,4' }).then((response) => {
  171. const treeData = fommat({
  172. arrayList: response.data.data,
  173. pidStr: "parent_id",
  174. });
  175. const accessedRouters = filterAsyncRouter(treeData, roles)
  176. const finalRouters = filterEmptyDirectory(accessedRouters)
  177. commit('SET_ROUTERS', finalRouters)
  178. resolve()
  179. });
  180. })
  181. },
  182. },
  183. }
  184. export default permission

指令权限

封装了一个非常方便实现按钮级别权限的自定义指令。
使用案例:

  1. <template>
  2. <!-- 校验是否有 dashboard 权限下的 add 操作权限 -->
  3. <a-button v-action:add >添加用户</a-button>
  4. <!-- 校验是否有 dashboard 权限下的 del 操作权限 -->
  5. <a-button v-action:del>删除用户</a-button>
  6. <!-- 校验是否有 dashboard 权限下的 edit 操作权限 -->
  7. <a v-action:edit @click="edit(record)">修改</a>
  8. </template>

需要注意的是,指令权限默认从 store 中获取当前已经登陆的用户的角色和权限信息进行比对,所以也要对指令权限的获取和校验 Action 权限部分进行自定义。
在某些情况下,不适合使用 v-action,例如 Tab 组件,只能通过手动设置 v-if 来实现。
这时候,为其提供了原始 v-if 级别的权限判断。

  1. <template>
  2. <a-tabs>
  3. <a-tab-pane v-if="$auth('dashboard.add')" tab="Tab 1">
  4. some context..
  5. </a-tab-pane>
  6. <a-tab-pane v-if="$auth('dashboard.del')" tab="Tab 2">
  7. some context..
  8. </a-tab-pane>
  9. <a-tab-pane v-if="$auth('dashboard.edit')" tab="Tab 3">
  10. some context..
  11. </a-tab-pane>
  12. </a-tabs>
  13. </template>

以上代码的 if 判断会检查,当前登录用户是否存在 dashboard 下的 add / del / edit 权限