vue基於d2-admin的RBAC許可權管理解決方案

若邪發表於2019-01-06

前兩篇關於vue許可權路由文章的填坑,說了一堆理論,是時候操作一波了。

vue許可權路由實現方式總結

vue許可權路由實現方式總結二

選擇d2-admin是因為element-ui的相關開源專案裡,d2-admin的結構和程式碼是讓我感到最舒服的,而且基於d2-admin實現RBAC許可權管理也很方便,對d2-admin沒有大的侵入性的改動。

vue基於d2-admin的RBAC許可權管理解決方案

預覽地址

Github

vue基於d2-admin的RBAC許可權管理解決方案
vue基於d2-admin的RBAC許可權管理解決方案
vue基於d2-admin的RBAC許可權管理解決方案
vue基於d2-admin的RBAC許可權管理解決方案
vue基於d2-admin的RBAC許可權管理解決方案
vue基於d2-admin的RBAC許可權管理解決方案

相關概念

不瞭解RBAC,可以看這裡企業管理系統前後端分離架構設計 系列一 許可權模型篇

許可權模型

  • 實現了RBAC模型許可權控制
  • 選單與路由獨立管理,完全由後端返回
  • user儲存使用者
  • admin標識使用者是否為系統管理員
  • role儲存角色資訊
  • roleUser儲存使用者與角色的關聯關係
  • menu儲存選單資訊,型別分為選單功能,一個選單下可以有多個功能,選單型別的permission欄位標識訪問這個選單需要的功能許可權,功能型別的permission欄位相當於此功能的別稱,所以選單型別的permission欄位為其某個功能型別子節點的permission
  • permission儲存角色與功能的關聯關係
  • interface儲存介面資訊
  • functionInterface儲存功能與介面關聯關係,通過查詢使用者所屬角色,再查詢相關角色所具備的功能許可權,再通過相關功能就可以查出使用者所能訪問的介面
  • route儲存前端路由資訊,通過permission欄位過濾出使用者所能訪問的路由

執行流程及相關API

使用d2admin的原有登入邏輯,全域性路由守衛中判斷是否已經拉取許可權資訊,獲取後標識為已獲取。

const token = util.cookies.get('token')
    if (token && token !== 'undefined') {
      //拉取許可權資訊
      if (!isFetchPermissionInfo) {
        await fetchPermissionInfo();
        isFetchPermissionInfo = true;
        next(to.path, true)
      } else {
        next()
      }
    } else {
      // 將當前預計開啟的頁面完整地址臨時儲存 登入後繼續跳轉
      // 這個 cookie(redirect) 會在登入後自動刪除
      util.cookies.set('redirect', to.fullPath)
      // 沒有登入的時候跳轉到登入介面
      next({
        name: 'login'
      })
    }
複製程式碼
//標記是否已經拉取許可權資訊
let isFetchPermissionInfo = false

let fetchPermissionInfo = async () => {
  //處理動態新增的路由
  const formatRoutes = function (routes) {
    routes.forEach(route => {
      route.component = routerMapComponents[route.component]
      if (route.children) {
        formatRoutes(route.children)
      }
    })
  }
  try {
    let userPermissionInfo = await userService.getUserPermissionInfo()
    permissionMenu = userPermissionInfo.accessMenus
    permissionRouter = userPermissionInfo.accessRoutes
    permission.functions = userPermissionInfo.userPermissions
    permission.roles = userPermissionInfo.userRoles
    permission.interfaces = util.formatInterfaces(userPermissionInfo.accessInterfaces)
    permission.isAdmin = userPermissionInfo.isAdmin == 1
  } catch (ex) {
    console.log(ex)
  }
  formatRoutes(permissionRouter)
  let allMenuAside = [...menuAside, ...permissionMenu]
  let allMenuHeader = [...menuHeader, ...permissionMenu]
  //動態新增路由
  router.addRoutes(permissionRouter);
  // 處理路由 得到每一級的路由設定
  store.commit('d2admin/page/init', [...frameInRoutes, ...permissionRouter])
  // 設定頂欄選單
  store.commit('d2admin/menu/headerSet', allMenuHeader)
  // 設定側邊欄選單
  store.commit('d2admin/menu/fullAsideSet', allMenuAside)
  // 初始化選單搜尋功能
  store.commit('d2admin/search/init', allMenuHeader)
  // 設定許可權資訊
  store.commit('d2admin/permission/set', permission)
  // 載入上次退出時的多頁列表
  store.dispatch('d2admin/page/openedLoad')
  await Promise.resolve()
}
複製程式碼

後端需要返回的許可權資訊包括許可權過濾後的角色編碼集合,功能編碼集合,介面資訊集合,選單列表,路由列表,以及是否系統管理員標識。格式如下

{
  "statusCode": 200,
  "msg": "",
  "data": {
    "userName": "MenuManager",
    "userRoles": [
      "R_MENUADMIN"
    ],
    "userPermissions": [
      "p_menu_view",
      "p_menu_edit",
      "p_menu_menu"
    ],
    "accessMenus": [
      {
        "title": "系統",
        "path": "/system",
        "icon": "cogs",
        "children": [
          {
            "title": "系統設定",
            "icon": "cogs",
            "children": [
              {
                "title": "選單管理",
                "path": "/system/menu",
                "icon": "th-list"
              }
            ]
          },
          {
            "title": "組織架構",
            "icon": "pie-chart",
            "children": [
              {
                "title": "部門管理",
                "icon": "html5"
              },
              {
                "title": "職位管理",
                "icon": "opencart"
              }
            ]
          }
        ]
      }
    ],
    "accessRoutes": [
      {
        "name": "System",
        "path": "/system",
        "component": "layoutHeaderAside",
        "componentPath": "layout/header-aside/layout",
        "meta": {
          "title": "系統設定",
          "cache": true
        },
        "children": [
          {
            "name": "MenuPage",
            "path": "/system/menu",
            "component": "menu",
            "componentPath": "pages/sys/menu/index",
            "meta": {
              "title": "選單管理",
              "cache": true
            }
          },
          {
            "name": "RoutePage",
            "path": "/system/route",
            "component": "route",
            "componentPath": "pages/sys/route/index",
            "meta": {
              "title": "路由管理",
              "cache": true
            }
          },
          {
            "name": "RolePage",
            "path": "/system/role",
            "component": "role",
            "componentPath": "pages/sys/role/index",
            "meta": {
              "title": "角色管理",
              "cache": true
            }
          },
          {
            "name": "UserPage",
            "path": "/system/user",
            "component": "user",
            "componentPath": "pages/sys/user/index",
            "meta": {
              "title": "使用者管理",
              "cache": true
            }
          },
          {
            "name": "InterfacePage",
            "path": "/system/interface",
            "component": "interface",
            "meta": {
              "title": "介面管理"
            }
          }
        ]
      }
    ],
    "accessInterfaces": [
      {
        "path": "/menu/:id",
        "method": "get"
      },
      {
        "path": "/menu",
        "method": "get"
      },
      {
        "path": "/menu/save",
        "method": "post"
      },
      {
        "path": "/interface/paged",
        "method": "get"
      }
    ],
    "isAdmin": 0,
    "avatarUrl": "https://api.adorable.io/avatars/85/abott@adorable.png"
  }
}
複製程式碼

設定選單

將固定選單(/menu/header/menu/aside)與後端返回的許可權選單(accessMenus)合併後,存入相應的vuex store模組中

...
let allMenuAside = [...menuAside, ...permissionMenu]
let allMenuHeader = [...menuHeader, ...permissionMenu]
...
// 設定頂欄選單
store.commit('d2admin/menu/headerSet', allMenuHeader)
// 設定側邊欄選單
store.commit('d2admin/menu/fullAsideSet', allMenuAside)
// 初始化選單搜尋功能
store.commit('d2admin/search/init', allMenuHeader)
複製程式碼

處理路由

預設使用routerMapComponents 的方式處理後端返回的許可權路由

//處理動態新增的路由
const formatRoutes = function (routes) {
    routes.forEach(route => {
        route.component = routerMapComponents[route.component]
        if (route.children) {
        formatRoutes(route.children)
        }
    })
}
...
formatRoutes(permissionRouter)
//動態新增路由
router.addRoutes(permissionRouter);
// 處理路由 得到每一級的路由設定
store.commit('d2admin/page/init', [...frameInRoutes, ...permissionRouter])
複製程式碼

路由處理方式及區別可看vue許可權路由實現方式總結二

設定許可權資訊

將角色編碼集合,功能編碼集合,介面資訊集合,以及是否系統管理員標識存入相應的vuex store模組中

...
permission.functions = userPermissionInfo.userPermissions
permission.roles = userPermissionInfo.userRoles
permission.interfaces = util.formatInterfaces(userPermissionInfo.accessInterfaces)
permission.isAdmin = userPermissionInfo.isAdmin == 1
...
// 設定許可權資訊
store.commit('d2admin/permission/set', permission)
複製程式碼

介面許可權控制以及loading配置

支援使用角色編碼,功能編碼以及介面許可權進行控制,如下

export function getMenuList() {
    return request({
        url: '/menu',
        method: 'get',
        interfaceCheck: true,
        permission:["p_menu_view"],
        loading: {
            type: 'loading',
            options: {
                fullscreen: true,
                lock: true,
                text: '載入中...',
                spinner: 'el-icon-loading',
                background: 'rgba(0, 0, 0, 0.8)'
            }
        },
        success: {
            type: 'message',
            options: {
                message: '載入選單成功',
                type: 'success'
            }
        }
    })
}
複製程式碼

interfaceCheck: true表示使用介面許可權進行控制,如果vuex store中儲存的介面資訊與當前要請求的介面想匹配,則可發起請求,否則請求將被攔截。

permission:["p_menu_view"]表示使用角色編碼和功能編碼進行許可權校驗,如果vuex store中儲存的角色編碼或功能編碼與當前表示的編碼相匹配,則可發起請求,否則請求將被攔截。

原始碼位置在libs/permission.js,可根據自己需求進行修改

loading配置相關原始碼在libs/loading.js,根據自己需求進行配置,success也是如此,原始碼在libs/loading.js。 照此思路可以自行配置其它功能,比如請求失敗等。

頁面元素許可權控制

使用指令v-permission

 <el-button
    v-permission:function.all="['p_menu_edit']"
    type="primary"
    icon="el-icon-edit"
    size="mini"
    @click="batchEdit"
    >批量編輯</el-button>
複製程式碼

引數可為functionrole,表明以功能編碼或角色編碼進行校驗,為空則使用兩者進行校驗。

修飾符all,表示必須全部匹配指令值中所有的編碼。

原始碼位置在plugin/permission/index.js,根據自己實際需求進行修改。

使用v-if+全域性方法:

<el-button
    v-if="canAdd"
    type="primary"
    icon="el-icon-circle-plus-outline"
    size="mini"
    @click="add"
    >新增</el-button>
複製程式碼
data() {
    return {
      canAdd: this.hasPermissions(["p_menu_edit"])
    };
  },
複製程式碼

預設同時使用角色編碼與功能編碼進行校驗,有一項匹配即可。

類似的方法還要hasFunctionshasRoles

原始碼位置在plugin/permission/index.js,根據自己實際需求進行修改。

不要使用v-if="hasPermissions(['p_menu_edit'])"這種方式,會導致方法多次執行

也可以直接在元件中從vuex store讀取許可權資訊進行校驗。

開發建議

  • 頁面級別的元件放到pages/目錄下,並且在routerMapCompnonents/index.js中以key-value的形式匯出

  • 不需要許可權控制的固定選單放到menu/aside.jsmenu/header.js

  • 不需要許可權控制的路由放到router/routes.js frameIn

  • 需要許可權控制的選單與路由通過介面的管理功能進行新增,確保選單的path與路由的path相對應,路由的name與頁面元件的name一致才能使keep-alive生效,路由的componentrouterMapCompnonents/index.js中能通過key匹配到。

  • 開發階段選單與路由的新增可由開發人員自行維護,並維護一份清單,上線後將清單交給相關的人去維護即可。

如果覺得麻煩,不想選單與路由由後端返回,可以在前端維護一份選單和路由(路由中的component還是使用字串,參考mock/permissionMenuAndRouter.js),並且在選單和路由上面維護相應的許可權編碼,一般都是使用功能編碼。後端就不需要返回選單和路由資訊了,但是其他許可權資訊,比如角色編碼,功能編碼等還是需要的。通過後端返回的功能編碼列表,在前端過濾出使用者具備許可權的選單和路由,過濾處理後後的選單與路由格式與之前由後端返回的格式一致,然後將處理後的選單與路由當做後端返回的一樣處理即可。

資料mock與程式碼生成

資料mock使用lazy-mock修改而來的d2-admin-server,資料真實來源於後端,相比其他工具,支援資料持久化,儲存使用的是json檔案,不需要安裝資料庫。簡單的配置即可自動生成增刪改查的介面。

後端使用中介軟體控制訪問許可權,比如:

 .get('/menu', PermissionCheck(), controllers.menu.getMenuList)
複製程式碼

PermissionCheck預設使用介面進行校驗,校驗使用者所能訪問的API中是否匹配當前API,支援使用功能編碼與角色編碼進行校驗PermissionCheck(["p_menu_edit"],["r_menu_admin"],true),第一個引數為功能編碼,第二個為角色編碼,第三個為是否使用介面進行校驗。

更多詳細用法可看lazy-mock文件

前端程式碼生成還在開發中...

相關文章