前言
在一個項目中,一些功能會涉及到重要的數(shù)據(jù)管理舅踪,為了確保數(shù)據(jù)的安全,我們會在項目中加入權(quán)限來限制每個用戶的操作贷腕。作為前端咬展,我們要做的是配合后端給到的權(quán)限數(shù)據(jù),做頁面上的各種各樣的限制涮总。
需求
因為這是一個工作上的業(yè)務需求祷舀,所以對于我來說主要有兩個地方需要進行權(quán)限控制。
第一個是側(cè)邊菜單欄抛丽,需要控制顯示與隱藏饰豺。
第二個就是頁面內(nèi)的各個按鈕,彈窗等冤吨。
流程
-
如何獲取用戶權(quán)限漩蟆?
后端(當前用戶擁有的權(quán)限列表)-> 前端(通過后端的接口獲取到,下文中我們把當前用戶的權(quán)限列表叫做 permissionList)
-
前端如何做限制怠李?
通過產(chǎn)品的需求,在項目中進行權(quán)限點的配置夷蚊,然后通過 permissionList 尋找是否有配置的權(quán)限點翘簇,有就顯示,沒有就不顯示呜笑。
-
然后呢?
沒了凰慈。
當我剛開始接到這個需求的時候就是這么想的驼鹅,這有什么難的,不就獲取 permissionList 然后判斷就可以了嘛输钩。后來我才發(fā)現(xiàn)真正的需求遠比我想象的復雜买乃。
真正的問題
上面的需求有提到我們主要解決兩個問題,側(cè)邊菜單欄的顯示 & 頁面內(nèi)操作剪验。
假設(shè)我們有這樣一個路由的設(shè)置(以下只是一個例子):
import VueRouter from 'vue-router'
/* 注意:以下配置僅為部分配置功戚,并且省去了 component 的配置 */
export const routes = [
{
path: '/',
name: 'Admin',
label: '首頁'
},
{
path: '/user',
name: 'User',
label: '用戶',
redirect: { name: 'UserList' },
children: [
{
path: 'list',
name: 'UserList',
label: '用戶列表'
},
{
path: 'group',
name: 'UserGroup',
label: '用戶組',
redirect: { name: 'UserGroupList' },
children: [
{
path: 'list',
name: 'UserGroupList',
label: '用戶組列表'
},
{
path: 'config',
name: 'UserGroupConfig',
label: '用戶組設(shè)置'
}
]
}
]
},
{
path: '/setting',
name: 'Setting',
label: '系統(tǒng)設(shè)置'
},
{
path: '/login',
name: 'Login',
label: '登錄'
}
]
const router = new VueRouter({
routes
})
export default router
其中前兩級路由會顯示在側(cè)邊欄中,第三級就不會顯示在側(cè)邊欄中了届宠。
頁面內(nèi)操作的權(quán)限設(shè)置不需要考慮很多其他東西壳咕,我們主要針對側(cè)邊欄以及路由進行問題的分析,通過分析,主要有以下幾個問題:
什么時候獲取 permissionList寸谜,如何存儲 permissionList
子路由全都沒權(quán)限時不應該顯示本身(例:當用戶列表和用戶組都沒有權(quán)限時,用戶也不應該顯示在側(cè)邊欄)
默認重定向的路由沒有權(quán)限時他爸,應尋找 children 中有權(quán)限的一項重定向(例:用戶路由重定向到用戶列表路由果善,若用戶列表沒有權(quán)限,則應該重定向到用戶組路由)
當用戶直接輸入沒有權(quán)限的 url 時需要跳轉(zhuǎn)到?jīng)]有權(quán)限的頁面或其他操作讨跟。(路由限制)
下面我們針對以上問題一個一個解決。
什么時候獲取權(quán)限茶袒,存儲在哪 & 路由限制
我這里是在 router
的 beforeEach
中獲取的凉馆,獲取的permissionList是存放在 vuex
中。
原因是考慮到要做路由的限制向叉,以及方便后面項目中對權(quán)限列表的使用嗦董,以下是實現(xiàn)的示例:
首先我們加入權(quán)限配置到 router 上:
// 以下只展示部分配置
{
path: '/user',
name: 'User',
label: '用戶',
meta: {
permissions: ['U_1']
},
redirect: { name: 'UserList' },
children: [
{
path: 'list',
name: 'UserList',
label: '用戶列表',
meta: {
permissions: ['U_1_1']
}
},
{
path: 'group',
name: 'UserGroup',
label: '用戶組',
meta: {
permissions: ['U_1_2']
},
redirect: { name: 'UserGroupList' },
children: [
{
path: 'list',
name: 'UserGroupList',
label: '用戶組列表',
meta: {
permissions: ['U_1_2_1']
}
},
{
path: 'config',
name: 'UserGroupConfig',
label: '用戶組設(shè)置',
meta: {
permissions: ['U_1_2_2']
}
}
]
}
]
}
可以看到我們把權(quán)限加在了 meta 上,是為了更簡單的從 router.beforeEch
中進行權(quán)限判斷销睁,權(quán)限設(shè)置為一個數(shù)組存崖,是因為一個頁面可能涉及多個權(quán)限。
接下來我們設(shè)置 router.beforeEach
:
// 引入項目的 vuex
import store from '@/store'
// 引入判斷是否擁有權(quán)限的函數(shù)
import { includePermission } from '@/utils/permission'
router.beforeEach(async (to, from, next) => {
// 先判斷是否為登錄冗栗,登錄了才能獲取到權(quán)限供搀,怎么判斷登錄就不寫了
if (!isLogin) {
try {
// 這里獲取 permissionList
await store.dispatch('getPermissionList')
// 這里判斷當前頁面是否有權(quán)限
const { permissions } = to.meta
if (permissions) {
const hasPermission = includePermission(permissions)
if (!hasPermission) next({ name: 'NoPermission' })
}
next()
}
} else {
next({ name: 'Login' })
}
})
我們可以看到我們需要一個判斷權(quán)限的方法 & vuex 中的 getPermissionList 如下:
// @/store
export default {
state: {
permissionList: []
},
mutations: {
updatePermissionList: (state, payload) => {
state.permissionList = payload
}
},
actions: {
getPermissionList: async ({ state, commit }) => {
// 這里是為了防止重復獲取
if (state.permissionList.length) return
// 發(fā)送請求方法省略
const list = await api.getPermissionList()
commit('updatePermissionList', list)
}
}
}
// @/utils/permission
import store from '@/store'
/**
* 判斷是否擁有權(quán)限
* @param {Array<string>} permissions - 要判斷的權(quán)限列表
*/
function includePermission (permissions = []) {
// 這里要判斷的權(quán)限沒有設(shè)置的話葛虐,就等于不需要權(quán)限,直接返回 true
if (!permissions.length) return true
const permissionList = store.state.permissionList
return !!permissions.find(permission => permissionList.includes(permission))
}
重定向問題
以上我們解決了路由的基本配置與權(quán)限如何獲取涕蚤,怎么限制路由跳轉(zhuǎn)的诵,接下來我們要處理的就是重定向問題了。
這一點可能和我們項目本身架構(gòu)有關(guān)烦粒,我們項目的側(cè)邊欄下還有子級代赁,是以下圖中的 tab 切換展現(xiàn)的兽掰,正常情況當點擊藥品管理后頁面會重定向到入庫管理的 tab 切換頁面义黎,但當入庫管理沒有權(quán)限時,則應該直接重定向到出庫管理界面泻云。
所以想實現(xiàn)以上的效果狐蜕,我需要重寫 router 的 redirect,做到可以動態(tài)判斷(因為在我配置路由時并不知道當前用戶的權(quán)限列表)
然后我查看了 vue-router 的文檔婆瓜,發(fā)現(xiàn)了 redirect 可以是一個方法贡羔,這樣就可以解決重定向問題了。
vue-router 中 redirect 說明 猴蹂,根據(jù)說明我們可以改寫 redirect 如下:
// 我們需要引入判斷權(quán)限方法
import { includePermission } from '@/utils/permission'
const children = [
{
path: 'list',
name: 'UserList',
label: '用戶列表',
meta: {
permissions: ['U_1_1']
}
},
{
path: 'group',
name: 'UserGroup',
label: '用戶組',
meta: {
permissions: ['U_1_2']
}
}
]
const routeDemo = {
path: '/user',
name: 'User',
label: '用戶',
redirect: (to) => {
if (includePermission(children[0].meta.permissions)) return { name: children[0].name }
if (includePermission(children[1].meta.permissions)) return { name: children[1].name }
},
children
}
雖然問題解決了楣嘁,但是發(fā)現(xiàn)這樣寫下去很麻煩,還要修改 router 的配置聋溜,所以我們使用一個方法生成:
// @/utils/permission
/**
* 創(chuàng)建重定向函數(shù)
* @param {Object} redirect - 重定向?qū)ο? * @param {string} redirect.name - 重定向的組件名稱
* @param {Array<any>} children - 子列表
*/
function createRedirectFn (redirect = {}, children = []) {
// 避免緩存太大叭爱,只保留 children 的 name 和 permissions
const permissionChildren = children.map(({ name = '', meta: { permissions = [] } = {} }) => ({ name, permissions }))
return function (to) {
// 這里一定不能在 return 的函數(shù)外面篩選,因為權(quán)限是異步獲取的
const hasPermissionChildren = permissionChildren.filter(item => includePermission(item.permissions))
// 默認填寫的重定向的 name
const defaultName = redirect.name || ''
// 如果默認重定向沒有權(quán)限涤伐,則從 children 中選擇第一個有權(quán)限的路由做重定向
const firstPermissionName = (hasPermissionChildren[0] || { name: '' }).name
// 判斷是否需要修改默認的重定向
const saveDefaultName = !!hasPermissionChildren.find(item => item.name === defaultName && defaultName)
if (saveDefaultName) return { name: defaultName }
else return firstPermissionName ? { name: firstPermissionName } : redirect
}
}
然后我們就可以改寫為:
// 我們需要引入判斷權(quán)限方法
import { includePermission, createRedirectFn } from '@/utils/permission'
const children = [
{
path: 'list',
name: 'UserList',
label: '用戶列表',
meta: {
permissions: ['U_1_1']
}
},
{
path: 'group',
name: 'UserGroup',
label: '用戶組',
meta: {
permissions: ['U_1_2']
}
}
]
const routeDemo = {
path: '/user',
name: 'User',
label: '用戶',
redirect: createRedirectFn({ name: 'UserList' }, children),
children
}
這樣稍微簡潔一些凝果,但我還是需要一個一個路由去修改睦尽,所以我又寫了一個方法來遞歸 router 配置,并重寫他們的 redirect:
// @/utils/permission
/**
* 創(chuàng)建有權(quán)限的路由配置(多級)
* @param {Object} config - 路由配置對象
* @param {Object} config.redirect - 必須是 children 中的一個山害,并且使用 name
*/
function createPermissionRouter ({ redirect, children = [], ...others }) {
const needRecursion = !!children.length
if (needRecursion) {
return {
...others,
redirect: createRedirectFn(redirect, children),
children: children.map(item => createPermissionRouter(item))
}
} else {
return {
...others,
redirect
}
}
}
這樣我們只需要在最外層的 router 配置加上這樣一層函數(shù)就可以了:
import { createPermissionRouter } from '@/utils/permission'
const routesConfig = [
{
path: '/user',
name: 'User',
label: '用戶',
meta: {
permissions: ['U_1']
},
redirect: { name: 'UserList' },
children: [
{
path: 'list',
name: 'UserList',
label: '用戶列表',
meta: {
permissions: ['U_1_1']
}
},
{
path: 'group',
name: 'UserGroup',
label: '用戶組',
meta: {
permissions: ['U_1_2']
},
redirect: { name: 'UserGroupList' },
children: [
{
path: 'list',
name: 'UserGroupList',
label: '用戶組列表',
meta: {
permissions: ['U_1_2_1']
}
},
{
path: 'config',
name: 'UserGroupConfig',
label: '用戶組設(shè)置',
meta: {
permissions: ['U_1_2_2']
}
}
]
}
]
}
]
export const routes = routesConfig.map(item => createPermissionRouter(item))
const router = new VueRouter({
routes
})
export default router
當然這樣寫還有一個好處浪慌,其實你并不需要設(shè)置 redirect,這樣會自動重定向到 children 的第一個有權(quán)限的路由
側(cè)邊欄顯示問題
我們的項目使用的是根據(jù)路由的配置來生成側(cè)邊欄的权纤,當然會加一些其他的參數(shù)來顯示顯示層級等問題汹想,這里就不寫具體代碼了,如何解決側(cè)邊欄 children 全都無權(quán)限不顯示的問題呢古掏。
這里我的思路是,把路由的配置也一同更新到 vuex 中丧枪,然后側(cè)邊欄配置從 vuex 中的配置來讀取庞萍。
由于這個地方涉及修改的東西有點多,而且涉及業(yè)務屎篱,我就不把代碼拿出來了葵蒂,你可以自行實驗。
方便團隊部署權(quán)限點的方法
以上我們解決了大部分權(quán)限的問題践付,那么還有很多涉及到業(yè)務邏輯的權(quán)限點的部署永高,所以為了團隊中其他人可以優(yōu)雅簡單的部署權(quán)限點到各個頁面中,我在項目中提供了以下幾種方式來部署權(quán)限:
- 通過指令
v-permission
來直接在 template 上設(shè)置
<div v-permission="['U_1']"></div>
- 通過全局方法
this.$permission
判斷命爬,因為有些權(quán)限并非在模版中的
{
hasPermission () {
// 通過方法 $permission 判斷是否擁有權(quán)限
return this.$permission(['U_1_1', 'U_1_2'])
}
}
這里要注意饲宛,為了 $permission
方法的返回值是可被監(jiān)測的,判斷時需要從 this.$store
中來判斷,以下為實現(xiàn)代碼:
// @/utils/permission
/**
* 判斷是否擁有權(quán)限
* @param {Array<string|number>} permissions - 要判斷的權(quán)限列表
* @param {Object} permissionList - 傳入 store 中的權(quán)限列表以實現(xiàn)數(shù)據(jù)可監(jiān)測
*/
function includePermissionWithStore (permissions = [], permissionList = []) {
if (!permissions.length) return true
return !!permissions.find(permission => permissionList.includes(permission))
}
import { includePermissionWithStore } from '@/utils/permission'
export default {
install (Vue, options) {
Vue.prototype.$permission = function (permissions) {
const permissionList = this.$store.state.permissionList
return includePermissionWithStore(permissions, permissionList)
}
}
}
以下為指令的實現(xiàn)代碼(為了不與 v-if 沖突久锥,這里控制顯示隱藏通過添加/移除 className 的方式):
// @/directive/permission
import { includePermission } from '@/utils/permission'
const permissionHandle = (el, binding) => {
const permissions = binding.value
if (!includePermission(permissions)) {
el.classList.add('hide')
} else {
el.classList.remove('hide')
}
}
export default {
inserted: permissionHandle,
update: permissionHandle
}
總結(jié)
針對之前的問題异剥,有以下的總結(jié):
-
什么時候獲取 permissionList,如何存儲 permissionList
router.beforeEach 獲取冤寿,存儲在 vuex疚沐。
-
子路由全都沒權(quán)限時不應該顯示本身(例:當用戶列表和用戶設(shè)置都沒有權(quán)限時,用戶也不應該顯示在側(cè)邊欄)
通過存儲路由配置到 vuex 中亮蛔,生成側(cè)邊欄設(shè)置,獲取權(quán)限后修改 vuex 中的配置控制顯示 & 隱藏辣吃。
-
默認重定向的路由沒有權(quán)限時芬探,應尋找 children 中有權(quán)限的一項重定向(例:用戶路由重定向到用戶列表路由,若用戶列表沒有權(quán)限哩簿,則應該重定向到用戶組路由)
通過
vue-router
中redirect
設(shè)置為Function
來實現(xiàn) -
當用戶直接輸入沒有權(quán)限的 url 時需要跳轉(zhuǎn)到?jīng)]有權(quán)限的頁面或其他操作酝静。(路由限制)
在 meta 中設(shè)置權(quán)限, router.beforeEach 中判斷權(quán)限别智。
以上就是我對于這次權(quán)限需求的大體解決思路與代碼實現(xiàn),可能并不是很完美讳窟,但還是希望可以幫助到你 _
作者:邪瓶張起靈
文章:https://juejin.im/post/5c7bae3ff265da2db27950f3