前兩篇關于vue權限路由文章的填坑,說了一堆理論,是時候操作一波了。
vue權限路由實作方式總結
vue權限路由實作方式總結二
選擇d2-admin是因為element-ui的相關開源項目裡,d2-admin的結構和代碼是讓我感到最舒服的,而且基于d2-admin實作RBAC權限管理也很友善,對d2-admin沒有大的侵入性的改動。
預覽位址
Github
相關概念
不了解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/[email protected]"
}
}
設定菜單
将固定菜單(
/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>
參數可為
function
role
,表明以功能編碼或角色編碼進行校驗,為空則使用兩者進行校驗。
修飾符
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"])
};
},
預設同時使用角色編碼與功能編碼進行校驗,有一項比對即可。
類似的方法還要
hasFunctions
,
hasRoles
。
plugin/permission/index.js
不要使用 v-if="hasPermissions(['p_menu_edit'])"
這種方式,會導緻方法多次執行
也可以直接在元件中從vuex store讀取權限資訊進行校驗。
開發建議
- 頁面級别的元件放到
目錄下,并且在pages/
中以key-value的形式導出routerMapCompnonents/index.js
- 不需要權限控制的固定菜單放到
和menu/aside.js
中menu/header.js
- 不需要權限控制的路由放到
router/routes.js
内frameIn
- 需要權限控制的菜單與路由通過界面的管理功能進行添加,確定菜單的
與路由的path
相對應,路由的path
與頁面元件的name
一緻才能使name
生效,路由的keep-alive
在component
中能通過key比對到。routerMapCompnonents/index.js
- 開發階段菜單與路由的添加可由開發人員自行維護,并維護一份清單,上線後将清單交給相關的人去維護即可。
如果覺得麻煩,不想菜單與路由由後端傳回,可以在前端維護一份菜單和路由(路由中的還是使用字元串,參考
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文檔
前端代碼生成還在開發中...