基于角色動態(tài)控制iview-admin的菜單權限

iview-admin的菜單問題

  • 菜單樹無法顯示的缺陷

    作者研究的iview-admin是當前最新版本2.0.0,發(fā)現(xiàn)如果一個菜單的兄弟菜單當前用戶無權訪問,可能導致整顆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,隨即整個菜單樹無法顯示。

  • 菜單權限無法動態(tài)控制

    通常情況下,一個基于角色的權限控制系統(tǒng)會在服務端維護一個角色列表,并為每個角色分配菜單權限。當一個用戶訪問系統(tǒng)時,其能訪問的菜單由該用戶的角色而定。

    Iview-admin也許只是出于演示目的,其菜單權限是通過菜單的access屬性靜態(tài)配置,我們可以把這個稱之為允許訪問的角色列表,如示例中的"一級功能",其限制了只有admin999這個角色可以訪問。然而,對一個可維護的系統(tǒng)而言,一個菜單分配給了哪些角色是服務端后臺動態(tài)配置。顯然,這種靜態(tài)配置無法支撐一個正常的生產系統(tǒng)。

iview-admin的路由和菜單

  • 路由

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

    src/router/index.js引入路由配置并創(chuàng)建路由對象,該路由對象會在每次路由訪問前根據當前用戶的access標識(角色列表)和路由配置中的access進行匹配,達到基于角色控制菜單訪問權限的目的;

    Iview-admin的啟動入口即src/main.js引入src/router/index.js創(chuàng)建的路由對象,將其傳給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的路由配置計算出用戶的最終菜單。

定制思路

  • 思路一:不做任何定制

    開發(fā)期指定每個菜單允許訪問的角色列表,在返回用戶信息時返回用戶角色,由iview-admin現(xiàn)有方式控制,但是會面臨"iview-admin的菜單問題"中的兩個問題,不僅有顯示缺陷而且角色無法擴展,基本不可用。

  • 思路二:基于后臺的角色權限,初始化完整的菜單

    從服務端獲取所有角色,以及每個角色能訪問的菜單,根據這些信息動態(tài)修改routers里菜單的access配置,使菜單的access等于完整的運行訪問的角色列表。但是由于"iview-admin的菜單問題"的第一個問題的存在,導致該方法不可行。

  • 思路三:基于后臺的角色權限,初始化必要的菜單

    如果菜單里是當前用戶都能訪問的菜單,那么可以稱之為必要的菜單,經研究,這樣的菜單可以繞開缺陷一。這樣的菜單其access屬性不需要配置,因為菜單的整個范圍已經是當前用戶可訪問的范圍。

    當然,如果有時間和能力直接解決其無法展現(xiàn)菜單樹的缺陷可以考慮思路二,但是,前端生成一顆經過權限過濾后的必要菜單將更干凈純粹。

定制方法

? 作者采用思路三。首先將路由配置一拆為二,分為系統(tǒng)默認路由配置和菜單路由配置;向后臺獲取當前用戶信息時,獲取用戶所能訪問的菜單,結合菜單路由配置生成一顆經過權限過濾后的菜單樹。

  1. 拆分路由配置

    從“iveiw-admin的路由和菜單”可知,src/router/routers.js文件里的配置肩負兩個職責:一是vue路由配置,二是菜單配置,現(xiàn)在我們要將菜單部分獨立出去。

    將src/router/routers.js拆分成routers.js和menus.js兩個文件,routers.js保留系統(tǒng)默認路由,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:如下所示:假如我們有系統(tǒng)功能,以及子功能菜單一覽和角色管理

    import Main from '@/components/main'
    export default [
      {
        path: '/system',
        name: '系統(tǒng)管理',
        meta: {
          showAlways: false,
          icon: 'md-menu',
          title: '系統(tǒng)功能'
        },
        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. 基于狀態(tài)更新菜單樹

    src/store/module/app.js里,在store里聲明permission,該屬性為當前用戶所能訪問的路由配置,并將其初始為系統(tǒng)默認路由配置;

    從"iview-admin的路由和菜單"一節(jié)我們說過,side-menu菜單樹內容是由app.js里的menuList這個getter來控制,所以調整menuList這個getter,使其基于permission生成菜單列表,如下所示:

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

    到此,我們做到了菜單樹內容和permission狀態(tài)綁定

  2. 根據權限動態(tài)更新菜單樹

    經過以上的工作,菜單樹內容和表示用戶權限范圍的permission狀態(tài)進行了綁定,我們還需要在用戶登錄時去動態(tài)更新該permission,使菜單內容跟著當前登錄用戶變化而變化。

    首先,在src/store/module/app.js里添加修改permission狀態(tài)的mutation

    mutation如下,注意此處默認super_admin具有所有權限,可以根據自己業(yè)務修改:

        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接收的參數(shù)permission為當前用戶所能訪問的菜單列表,如['菜單一覽'],其邏輯為首先從menus.js克隆一份菜單路由配置,根據permission進行權限過濾,得到一個當前用戶所能訪問必要的菜單路由配置。

    cloneMenus實現(xiàn)了對菜單路由配置的克隆,其邏輯如下僅供參考:

    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
    }
    

    其次,在獲取用戶信息時觸發(fā)permission狀態(tài)更新

    在src/store/module/user.js里,在getUserInfo這個action實現(xiàn)的地方添加觸發(fā)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進行判斷,此時應該改成狀態(tài)管理里的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的服務端實現(xiàn)一定要根據用戶的角色,在user對象里返回permission,即用戶能訪問的菜單name組成的列表;

    本方法是根據用戶權限動態(tài)生成權限范圍內的菜單樹,已經不是通過前端路由的access配置來完成的權限過濾,所以menus.js里的菜單不需要再配置access;

參考代碼

? 沒想到這么多人希望要源碼

? 源碼參考https://github.com/tracelessman/iview-admin-menumanage-baseonroles,如果有價值請給個star。

? 注意,代碼中啟用了前端的mock,這樣我就不用把后端的代碼也上傳了,src/mock/login.js 給admin這個用戶添加了permission:['菜單一覽'],使其具有菜單一覽這個菜單權限。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容