基于角色动态控制iview-admin的菜单权限

iview-admin的菜单问题

  • 菜单树无法显示的缺陷

    作者研究的iview-admin是当前最新版本2.0.0,发现如果一个菜单的兄弟菜单当前用户无权访问,可能导致整颗side-menu菜单树无法显示。

    例如,可将如下配置粘贴到src/router/routers.js的路由配置中进行测试:

    {
      path: '/multilevel',
      name: 'multilevel',
      meta: {
        icon: 'md-menu',
        title: '多级菜单'
      },
      component: Main,
      children: [
        {
          path: 'level_2_3',
          name: 'level_2_3',
          meta: {
            icon: 'md-funnel',
            title: '二级-3'
          },
          component: () => import('@/view/multilevel/level-2-3.vue')
        }
      ]
    },
    {
      path: '/common',
      name: 'common',
      meta: {
        hideInBread: true
      },
      component: Main,
      children: [
        {
          path: 'default',
          name: '一级功能',
          meta: {
            access: ['admin999'],
            icon: 'ios-book',
            title: '一级功能'
          },
          component: () => import('@/view/common/default.vue')
        }
      ]
    },
    

    "多级菜单"的整个菜单是任何用户都有权访问的,但是common下的"一级菜单"配置了当前用户无法访问的access,随即整个菜单树无法显示。

  • 菜单权限无法动态控制

    通常情况下,一个基于角色的权限控制系统会在服务端维护一个角色列表,并为每个角色分配菜单权限。当一个用户访问系统时,其能访问的菜单由该用户的角色而定。

    Iview-admin也许只是出于演示目的,其菜单权限是通过菜单的access属性静态配置,我们可以把这个称之为允许访问的角色列表,如示例中的"一级功能",其限制了只有admin999这个角色可以访问。然而,对一个可维护的系统而言,一个菜单分配给了哪些角色是服务端后台动态配置。显然,这种静态配置无法支撑一个正常的生产系统。

iview-admin的路由和菜单

  • 路由

    Iview-admin的路由其实也是vue的路由,其在src/router/routers.js进行路由配置;

    src/router/index.js引入路由配置并创建路由对象,该路由对象会在每次路由访问前根据当前用户的access标识(角色列表)和路由配置中的access进行匹配,达到基于角色控制菜单访问权限的目的;

    Iview-admin的启动入口即src/main.js引入src/router/index.js创建的路由对象,将其传给vue,从而初始化整个前端。

  • 菜单

    Iview-admin的菜单由src/view/components/main/components/side-menu组件负责显示,其显示内容由menu-list属性指定;

    src/view/components/main/main.vue引用该组件,通过store 的getter将定义在src/store/module/app.js中的menuList返回值指定为side-menu组件的menu-list;

    menuList这个getter也是根据当前用户的角色以及src/router/routers.js的路由配置计算出用户的最终菜单。

定制思路

  • 思路一:不做任何定制

    开发期指定每个菜单允许访问的角色列表,在返回用户信息时返回用户角色,由iview-admin现有方式控制,但是会面临"iview-admin的菜单问题"中的两个问题,不仅有显示缺陷而且角色无法扩展,基本不可用。

  • 思路二:基于后台的角色权限,初始化完整的菜单

    从服务端获取所有角色,以及每个角色能访问的菜单,根据这些信息动态修改routers里菜单的access配置,使菜单的access等于完整的运行访问的角色列表。但是由于"iview-admin的菜单问题"的第一个问题的存在,导致该方法不可行。

  • 思路三:基于后台的角色权限,初始化必要的菜单

    如果菜单里是当前用户都能访问的菜单,那么可以称之为必要的菜单,经研究,这样的菜单可以绕开缺陷一。这样的菜单其access属性不需要配置,因为菜单的整个范围已经是当前用户可访问的范围。

    当然,如果有时间和能力直接解决其无法展现菜单树的缺陷可以考虑思路二,但是,前端生成一颗经过权限过滤后的必要菜单将更干净纯粹。

定制方法

​ 作者采用思路三。首先将路由配置一拆为二,分为系统默认路由配置和菜单路由配置;向后台获取当前用户信息时,获取用户所能访问的菜单,结合菜单路由配置生成一颗经过权限过滤后的菜单树。

  1. 拆分路由配置

    从“iveiw-admin的路由和菜单”可知,src/router/routers.js文件里的配置肩负两个职责:一是vue路由配置,二是菜单配置,现在我们要将菜单部分独立出去。

    将src/router/routers.js拆分成routers.js和menus.js两个文件,routers.js保留系统默认路由,menus.js为菜单路由。routers.js只负责login、home、error_401、error_500、error_404这几个路由,如下所示:

    import Main from '@/components/main'
    
    export default [
      {
        path: '/login',
        name: 'login',
        meta: {
          title: 'Login - 登录',
          hideInMenu: true
        },
        component: () => import('@/view/login/login.vue')
      },
      {
        path: '/',
        name: '_home',
        redirect: '/home',
        component: Main,
        meta: {
          hideInMenu: true,
          notCache: true
        },
        children: [
          {
            path: '/home',
            name: 'home',
            meta: {
              hideInMenu: true,
              title: '首页',
              notCache: true,
              icon: 'md-home'
            },
            component: () => import('@/view/single-page/home')
          }
        ]
      },
      {
        path: '/401',
        name: 'error_401',
        meta: {
          hideInMenu: true
        },
        component: () => import('@/view/error-page/401.vue')
      },
      {
        path: '/500',
        name: 'error_500',
        meta: {
          hideInMenu: true
        },
        component: () => import('@/view/error-page/500.vue')
      },
      {
        path: '*',
        name: 'error_404',
        meta: {
          hideInMenu: true
        },
        component: () => import('@/view/error-page/404.vue')
      }
    ]
    

    menus.js负责菜单相关的路由配置,此出的菜单不要配置任何access:如下所示:假如我们有系统功能,以及子功能菜单一览和角色管理

    import Main from '@/components/main'
    export default [
      {
        path: '/system',
        name: '系统管理',
        meta: {
          showAlways: false,
          icon: 'md-menu',
          title: '系统功能'
        },
        component: Main,
        children: [
          {
            path: 'menumanage',
            name: '菜单一览',
            meta: {
              icon: 'md-funnel',
              title: '菜单一览'
            },
            component: () => import('@/view/system/menumng.vue')
          }
        ]
      }
    ]
    
    
  1. 路由对象接管所有路由

    经过拆分后,路由配置就分成里两部分,所以还要改造路由对象使其管理所有路由。

    src/router/index.js里引入menus.js,在new Router的地方向Router传入routers.js和menus.js的合并值,如:

    import routes from './routers'
    import menus from './menus'
    let allMenus = routes.concat(menus)
    const router = new Router({
      routes: allMenus,
      mode: 'history'
    })
    
  1. 基于状态更新菜单树

    src/store/module/app.js里,在store里声明permission,该属性为当前用户所能访问的路由配置,并将其初始为系统默认路由配置;

    从"iview-admin的路由和菜单"一节我们说过,side-menu菜单树内容是由app.js里的menuList这个getter来控制,所以调整menuList这个getter,使其基于permission生成菜单列表,如下所示:

    state: {
        permission: routers
      },
    getters: {
      menuList: (state, getters, rootState) => getMenuByRouter(state.permission, rootState.user.access)
    },
    

    到此,我们做到了菜单树内容和permission状态绑定

  2. 根据权限动态更新菜单树

    经过以上的工作,菜单树内容和表示用户权限范围的permission状态进行了绑定,我们还需要在用户登录时去动态更新该permission,使菜单内容跟着当前登录用户变化而变化。

    首先,在src/store/module/app.js里添加修改permission状态的mutation

    mutation如下,注意此处默认super_admin具有所有权限,可以根据自己业务修改:

        setPermission (state, {name,permission}) {
          if(name=='super_admin'){
            state.permission = routers.concat(menus)
          }else{
            let newMenus = cloneMenus(menus)
            let filteredMenus = filterMenus(newMenus,permission)
            state.permission = routers.concat(filteredMenus)
          }
        }
    

    其中setPermission这个mutation接收的参数permission为当前用户所能访问的菜单列表,如['菜单一览'],其逻辑为首先从menus.js克隆一份菜单路由配置,根据permission进行权限过滤,得到一个当前用户所能访问必要的菜单路由配置。

    cloneMenus实现了对菜单路由配置的克隆,其逻辑如下仅供参考:

    const cloneMenu = function (newMenus, {path, name, meta, component, children}) {
      let obj = {path,name,meta,component}
      newMenus.push(obj)
      if(children&&children.forEach){
        obj.children = []
        children.forEach(function (child) {
          cloneMenu(obj.children,child)
        })
      }
    }
    
    const cloneMenus = function (menus) {
      let newMenus = []
      menus.forEach(function (menu) {
        cloneMenu(newMenus,menu)
      })
      return newMenus
    }
    

    filterMenus对克隆出的菜单路由配置进行权限过滤,将不能访问的菜单进行删除,只保留最小集合:

    const filterMenu = function (menu,targets) {
      if(menu.children){
        for(let i=0;i<menu.children.length;i++){
          let remain = filterMenu(menu.children[i],targets)
          if(remain===false){
            menu.children.splice(i,1)
            i--
          }
        }
        if(menu.children.length===0){
          return false
        }
      }else if(!targets||targets.indexOf(menu.name)==-1){
        return false
      }
    }
    const filterMenus = function (menus,targets) {
      for(let i=0;i<menus.length;i++){
        let remain = filterMenu(menus[i],targets)
        if(remain===false){
          menus.splice(i,1)
          i--
        }
      }
      return menus
    }
    

    其次,在获取用户信息时触发permission状态更新

    在src/store/module/user.js里,在getUserInfo这个action实现的地方添加触发setPermission的逻辑,使菜单内容根据用户变化而变化,这里需要特别注意的是getUserInfo的服务端逻辑在user对象里一定要传permission,即用户能访问的菜单name组成的列表,如['菜单一览','角色管理']:

        getUserInfo ({ state, commit }) {
          return new Promise((resolve, reject) => {
            try {
              getUserInfo(state.token).then(res => {
                const data = res.data
                commit('setAvatar', avatar)
                commit('setUserName', data.name)
                commit('setUserId', data.user_id)
                commit('setAccess', data.access||[])
                commit('setHasGetInfo', true)
                commit('setPermission', data)
                resolve(data)
              }).catch(err => {
                reject(err)
              })
            } catch (error) {
              reject(error)
            }
          })
        }
    
  3. 调整路由权限过滤

    路由对象在路由访问时都要先进行权限判断,这个判断在src/router/index.js的turnTo方法中:

    const turnTo = (to, access, next) => {
      if (canTurnTo(to.name, access, routes)) next() // 有权限,可访问
      else next({ replace: true, name: 'error_401' }) // 无权限,重定向到401页面
    }
    

    canTurnTo方法基于routes进行判断,此时应该改成状态管理里的permission即用户有权访问的所有路由,如下所示:

    const turnTo = (to, access, next) => {
      if (canTurnTo(to.name, access, store.state.app.permission)) next() // 有权限,可访问
      else next({ replace: true, name: 'error_401' }) // 无权限,重定向到401页面
    }
    
  4. 其他

    经过以上步骤前端改造就此完成。

    需要注意是:

    getUserInfo的服务端实现一定要根据用户的角色,在user对象里返回permission,即用户能访问的菜单name组成的列表;

    本方法是根据用户权限动态生成权限范围内的菜单树,已经不是通过前端路由的access配置来完成的权限过滤,所以menus.js里的菜单不需要再配置access;

参考代码

​ 没想到这么多人希望要源码

​ 源码参考https://github.com/tracelessman/iview-admin-menumanage-baseonroles,如果有价值请给个star。

​ 注意,代码中启用了前端的mock,这样我就不用把后端的代码也上传了,src/mock/login.js 给admin这个用户添加了permission:['菜单一览'],使其具有菜单一览这个菜单权限。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容