• vue element admin 动态路由,后端thinkphp5.1托管权限和路由


    转载请注明出处 https://www.cnblogs.com/leesen/p/14640405.html,或将追究侵权责任

    一、背景

    背景部分引自 vue element admin 花裤衩

    先说一说我权限控制的主体思路,前端会有一份路由表,它表示了每一个路由可访问的权限。当用户登录之后,通过 token 获取用户的 role ,动态根据用户的 role 算出其对应有权限的路由,再通过router.addRoutes动态挂载路由。但这些控制都只是页面级的,说白了前端再怎么做权限控制都不是绝对安全的,后端的权限验证是逃不掉的。

    我司现在就是前端来控制页面级的权限,不同权限的用户显示不同的侧边栏和限制其所能进入的页面(也做了少许按钮级别的权限控制),后端则会验证每一个涉及请求的操作,验证其是否有该操作的权限,每一个后台的请求不管是 get 还是 post 都会让前端在请求 header里面携带用户的 token,后端会根据该 token 来验证用户是否有权限执行该操作。若没有权限则抛出一个对应的状态码,前端检测到该状态码,做出相对应的操作。


    作者:花裤衩
    链接:https://juejin.cn/post/6844903478880370701
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
     
     
    二、后端部分
     
     
    既然后端托管,那么后端除了用户表之外,就还需要菜单表和身份表
    mysql表结构如下:
    菜单表
    CREATE TABLE `cmf_ls_menu` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(255) DEFAULT NULL,
      `path` varchar(255) DEFAULT '#',
      `pid` int(11) DEFAULT '0',
      `url` varchar(255) DEFAULT '#',
      `redirect` varchar(255) DEFAULT '#',
      `hidden` varchar(10) DEFAULT NULL,
      `alwaysShow` varchar(10) DEFAULT NULL,
      `meta` text,
      `component` varchar(255) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;

    *菜单表的各个字段对应的是前端route/index.js对应的格式,字段意义同前端,其中pid表示菜单上级的id,顶级菜单为0

    用户表

    CREATE TABLE `cmf_ls_user` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `username` varchar(255) DEFAULT NULL,
      `avatar` varchar(255) DEFAULT NULL,
      `name` varchar(255) DEFAULT NULL,
      `password` varchar(255) DEFAULT NULL,
      `create_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
      `roles` varchar(255) DEFAULT '[]',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

    *其中 roles 是 cmf_ls_user_role 表的id的集合,这里是用一个字段简单实现,复杂业务可能需要有一张用户和身份的对照表

    身份表

    CREATE TABLE `cmf_ls_user_role` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(255) DEFAULT NULL,
      `description` varchar(255) DEFAULT NULL,
      `menu_ids` varchar(255) DEFAULT NULL,
      `create_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

    *menu_ids 是 cmf_ls_menu 表的id的集合

    其余php控制器、模型等就不在贴出来了,以下把主要的前端获取动态路由的返回的数据格式贴出来:

    {
        "code": 200,
        "msg": "",
        "data": [{
            "id": 1,
            "name": "users",
            "path": "/users",
            "pid": 0,
            "url": "#",
            "redirect": "/users/list",
            "hidden": false,
            "alwaysShow": false,
            "meta": {
                "icon": "el-icon-s-help",
                "status": true,
                "title": "RBAC权限管理"
            },
            "component": "#",
            "label": "RBAC权限管理",
            "children": [{
                "id": 2,
                "name": "userlist",
                "path": "userlist",
                "pid": 1,
                "url": "#",
                "redirect": "#",
                "hidden": false,
                "alwaysShow": false,
                "meta": {
                    "icon": "el-icon-s-help",
                    "status": true,
                    "title": "用户列表"
                },
                "component": "/rbac/userlist",
                "label": "用户列表",
                "children": []
            }, {
                "id": 3,
                "name": "menulist",
                "path": "menulist",
                "pid": 1,
                "url": "#",
                "redirect": "#",
                "hidden": false,
                "alwaysShow": false,
                "meta": {
                    "icon": "el-icon-s-help",
                    "status": true,
                    "title": "菜单管理"
                },
                "component": "/rbac/menulist",
                "label": "菜单管理",
                "children": []
            }, {
                "id": 10,
                "name": "RoleList",
                "path": "rolelist",
                "pid": 1,
                "url": "#",
                "redirect": "#",
                "hidden": false,
                "alwaysShow": false,
                "meta": {
                    "icon": "",
                    "status": true,
                    "title": "身份管理"
                },
                "component": "/rbac/rolelist",
                "label": "身份管理",
                "children": []
            }]
        }, {
            "id": 4,
            "name": "icon",
            "path": "/icon",
            "pid": 0,
            "url": "#",
            "redirect": "#",
            "hidden": false,
            "alwaysShow": false,
            "meta": {
                "icon": "icon",
                "status": true,
                "title": "icon"
            },
            "component": "#",
            "label": "icon",
            "children": [{
                "id": 9,
                "name": "Icons",
                "path": "index",
                "pid": 4,
                "url": "#",
                "redirect": "#",
                "hidden": false,
                "alwaysShow": false,
                "meta": {
                    "icon": "icon",
                    "status": true,
                    "title": "Icons"
                },
                "component": "/icons/index",
                "label": "Icons",
                "children": []
            }]
        }, {
            "id": 5,
            "name": "Permission",
            "path": "/permission",
            "pid": 0,
            "url": "#",
            "redirect": "/permission/page",
            "hidden": false,
            "alwaysShow": false,
            "meta": {
                "icon": "lock",
                "status": true,
                "title": "Permission"
            },
            "component": "#",
            "label": "Permission",
            "children": [{
                "id": 6,
                "name": "PagePermission",
                "path": "page",
                "pid": 5,
                "url": "#",
                "redirect": "#",
                "hidden": false,
                "alwaysShow": false,
                "meta": {
                    "icon": "",
                    "status": true,
                    "title": "Page Permission"
                },
                "component": "/permission/page",
                "label": "Page Permission",
                "children": []
            }, {
                "id": 7,
                "name": "DirectivePermission",
                "path": "directive",
                "pid": 5,
                "url": "#",
                "redirect": "#",
                "hidden": false,
                "alwaysShow": false,
                "meta": {
                    "icon": "",
                    "status": true,
                    "title": "Directive Permission"
                },
                "component": "/permission/directive",
                "label": "Directive Permission",
                "children": []
            }]
        }]
    }

    php部分实现了无限级菜单,用户多选身份,身份下配置菜单权限,从而达到不同身份不同菜单的结果。

    三、前端部分

    首先前端需要把/src/router/index.js 中的asyncRouters即动态路由部分清空

    export const asyncRoutes = [];

    然后在/src/api/user.js中添加如下方法,定义一个获取后端动态菜单的接口,接口路径和参数换成你自己的

    export function getAuthMenu(data){
      return request({
        url: '/ls/menu/getAuthMenuByRoles',
        method: 'post',
        data
      })
    }

    下面是/src/store/modules/permission.js的完整代码

    import { asyncRoutes, constantRoutes } from '@/router'
    import { getAuthMenu } from '@/api/user'
    import Layout from '@/layout'
    
    /**
     * Use meta.role to determine if the current user has permission
     * @param roles
     * @param route
     */
    function hasPermission(roles, route) {
      if (route.meta && route.meta.roles) {
        return roles.some(role => route.meta.roles.includes(role))
      } else {
        return true
      }
    }
    
    export const loadView = view => {
      // 路由懒加载
      return resolve => require(['@/views' + view], resolve)
    }
    /**
     * 后台查询的菜单数据拼装成路由格式的数据
     * @param routes
     */
    export function generaMenu(routes, data) {
      data.forEach(item => {
        const menu = {
          path: item.path,
          component: item.component === '#' ? Layout : loadView(item.component),
          hidden: item.hidden,
          children: [],
          name : item.name,
          meta: item.meta
        }
        if(item.pid == 0){
          menu.redirect = item.redirect;
        }
    
        if (item.children) {
          generaMenu(menu.children, item.children)
        }
        routes.push(menu)
      })
      routes.push({ path: '*', redirect: '/404', hidden: true })
    }
    
    /**
     * Filter asynchronous routing tables by recursion
     * @param routes asyncRoutes
     * @param roles
     */
    export function filterAsyncRoutes(routes, roles) {
      const res = []
    
      routes.forEach(route => {
        const tmp = { ...route }
        if (hasPermission(roles, tmp)) {
          if (tmp.children) {
            tmp.children = filterAsyncRoutes(tmp.children, roles)
          }
          res.push(tmp)
        }
      })
    
      return res
    }
    
    const state = {
      routes: [],
      addRoutes: []
    }
    
    const mutations = {
      SET_ROUTES: (state, routes) => {
        state.addRoutes = routes
        state.routes = constantRoutes.concat(routes)
      }
    }
    
    const actions = {
      generateRoutes({ commit }, roles) {
        return new Promise(resolve => {
          const loadMenuData = []
          getAuthMenu(state.token).then(response => {
            let data = response
            if (response.code !== 200) {
              alert(JSON.stringify('菜单数据加载异常'))
            } else {
              data = response.data
              Object.assign(loadMenuData, data)
              const tempAsyncRoutes = Object.assign([], asyncRoutes)
              generaMenu(tempAsyncRoutes, loadMenuData)
              let accessedRoutes
              if (roles.includes('admin')) {
                accessedRoutes = tempAsyncRoutes || []
              } else {
                accessedRoutes = filterAsyncRoutes(tempAsyncRoutes, roles)
              }
              commit('SET_ROUTES', accessedRoutes)
              resolve(accessedRoutes)
            }
          }).catch(error => {
            console.log(error)
          })
        })
      }
    }
    
    export default {
      namespaced: true,
      state,
      mutations,
      actions
    }

    里面的 generateRoutes 方法 和 generaMenu 方法是用来获取后端返回的菜单,然后处理成前端需要的router的格式

    最后是后台菜单界面截图和说明

    用户管理

     身份设置

     身份管理

    身份权限设置

     这里使用了 el-tree ,选中菜单后给后台传递的就是,tree.getCheckedKeys(),也就是包含各个选中菜单的id的集合(子级全选那么父级也会选中,子级没有全选那么父级会是半选,此时获取的选中key里就不会有父级,我们就将这个数据原样传递给后台。后台返回的动态菜单里,需要递归往上根据菜单PID判断查找父级的id,最终实现即时子菜单不是全选的,返回的菜单里也有父级,当然子菜单一个都没选的话,父级肯定也不需要了,否则前端的菜单是不会展示没父级的选中菜单的)

    然后是菜单的管理

     

     pid因为是无限级,所以后台需要返回如下树结构

     其中 顶级菜单 是前端固定拼到后台给的树结构中的,可能会如下图

    其中splpre字段就是子菜单的前缀,顶级无前缀,下级有一个 ' - ',再下级就是 ‘ - - ’,以此类推(这里是后端给的,主要为了让树结构看起来清晰)

    需要特别说明的是添加菜单这里,如果是顶级菜单无下级,那么肯定需要在component里填上组件的路径,如文件路径 /src/view/user/index.vue ,就需要在component字段写上 /user/index,因为permission.js中loadview方法拼接了模板;如果是父级菜单有下级,那么component填上#就行。

    另外需要特别说明的是 redirect字段,如果是顶级菜单有下级,则需要填上默认能跳转到子菜单的path。

    如果前端改完后报 Module build failed (from ./node_modules/eslint-loader/index.js): TypeError: Cannot read property 'range' of null,

    一般是由下图部分引起的(如果使用() => import(`@/views${item.component}`)导入的话)

     本人亲测,通过对babel-eslint 降级到7.2.3成功处理这个问题。。。具体方法百度

    至此简易的后台配置vue element admin 动态菜单 动态路由 动态权限 就完成了。

  • 相关阅读:
    2019/09/26,经济和科技
    失败的总和
    2019/11/05,现代人的焦虑
    2019/09/16,回忆和希望
    2019/09/13,捷径
    演讲手势
    因果谬论和基于数据的另一种说法
    文本框输入事件:onchange 、onblur 、onkeyup 、oninput
    开关按钮切换
    全选,反选,全不选
  • 原文地址:https://www.cnblogs.com/leesen/p/14640405.html
Copyright © 2020-2023  润新知