Vue實(shí)現(xiàn)按鈕級(jí)權(quán)限方案
在年初開(kāi)發(fā)一個(gè)中后臺(tái)管理系統(tǒng),功能涉及到了各個(gè)部門(產(chǎn)品、客服、市場(chǎng)等等),在開(kāi)始的版本中,我和后端配合使用了花褲衩手摸手系列的權(quán)限方案,前期非常nice,但是慢慢的隨著功能增多、業(yè)務(wù)越來(lái)越復(fù)雜,就變得有些吃力了,因?yàn)槲覀兊臋?quán)限動(dòng)態(tài)性太大了
- 手摸手系列權(quán)限方案是有比較清晰的權(quán)限劃分的,而我們公司部門的崗位職責(zé)有時(shí)比較模糊。
- 后端采用RBAC權(quán)限方案,為了達(dá)到第1點(diǎn)要求,將角色劃分的很細(xì),并且角色有時(shí)頻繁變動(dòng),導(dǎo)致每一次前端都需要手動(dòng)維護(hù)
- 為了解決上面2個(gè)痛點(diǎn),我將原方案進(jìn)行了一丟丟改造。
- 前端不再以角色來(lái)控制權(quán)限,而是以更小粒度的操作(接口)來(lái)控制,也就是前端不關(guān)心角色
- 路由還是由前端維護(hù)(我們的后端很排斥維護(hù)和他們不相干的東西:joy:),但改為通過(guò)操作列表對(duì)權(quán)限路由進(jìn)行過(guò)濾
- 使用單一的方式(方便維護(hù))控制頁(yè)面的局部權(quán)限,不再使用自定義指令方式,而是通過(guò)函數(shù)式組件,原因是使用自定義指令有多余的開(kāi)銷(插入再移除)
后端的配合:
routerName
有一些注意點(diǎn):
- 比如一個(gè)有權(quán)限的列表頁(yè)面A,同時(shí)這個(gè)列表接口被權(quán)限頁(yè)面B使用,現(xiàn)在你配置權(quán)限讓某一個(gè)用戶沒(méi)有A頁(yè)面權(quán)限,但可以使用B頁(yè)面,如果你的本意是可以使用B頁(yè)面的所有功能,這時(shí)就會(huì)有問(wèn)題,所以盡量不要將權(quán)限接口跨頁(yè)面使用,需要分清哪些數(shù)據(jù)需要通過(guò)字典接口獲取還是通過(guò)權(quán)限接口獲取
- 有些人可能會(huì)糾結(jié),前端維護(hù)權(quán)限安全嗎?肯定是不安全的,安全性主要還在后端這邊把控,后端做好數(shù)據(jù)和接口方面的權(quán)限控制,前端做權(quán)限控制我認(rèn)為主要還是為了交互體驗(yàn)等。沒(méi)有權(quán)限你為什么要讓我看到那一坨?
- 在使用這種方式之前,要明確當(dāng)前場(chǎng)景是否確實(shí)需要這么做,畢竟在項(xiàng)目比較大且接口很多的情況下,你跟操作碼之間有一場(chǎng)持久戰(zhàn)
實(shí)現(xiàn)
操作列表示例
以Restful風(fēng)格接口為例
const operations = [ { url: '/xxx', type: 'get', name: '查詢xxx', routeName: 'route1', // 接口對(duì)應(yīng)的路由 opcode: 'XXX_GET' // 操作碼,不變的 }, { url: '/xxx', type: 'post', name: '新增xxx', routeName: 'route1', opcode: 'XXX_POST' }, // ...... ]
路由的變化
在路由的 meta 中增加一個(gè)配置字段如 requireOps ,值可能為 String 或者 Array ,這表示當(dāng)前路由頁(yè)面要顯示的必要的操作碼, Array 類型是為了處理一個(gè)路由頁(yè)面需要滿足同時(shí)存在多個(gè)操作權(quán)限時(shí)才顯示的情況。若值不為這2種則視為無(wú)權(quán)限控制,任何用戶都能訪問(wèn)
由于最終需要根據(jù)過(guò)濾后的權(quán)限路由動(dòng)態(tài)生成菜單,所以還需要在路由選項(xiàng)中增加幾個(gè)字段處理顯示問(wèn)題,其中 hidden 優(yōu)先級(jí)大于 visible
hidden visible const permissionRoutes = [ { // visible: false, // hidden: true, path: '/xxx', name: 'route1', meta: { title: '路由1', requireOps: 'XXX_GET' }, // ... } ]
由于路由在前端維護(hù),所以以上配置只能寫死,如果后端能同意維護(hù)這一份路由表,那就可以有很多的發(fā)揮空間了,體驗(yàn)也能做的更好。
權(quán)限路由過(guò)濾
先將權(quán)限路由規(guī)范一下,同時(shí)保留一個(gè)副本,可能在可視化時(shí)需要用到
const routeMap = (routes, cb) => routes.map(route => { if (route.children && route.children.length > 0) { route.children = routeMap(route.children, cb) } return cb(route) }) const hasRequireOps = ops => Array.isArray(ops) || typeof ops === 'string' const normalizeRequireOps = ops => hasRequireOps(ops) ? [].concat(...[ops]) : null const normalizeRouteMeta = route => { const meta = route.meta = { ...(route.meta || {}) } meta.requireOps = normalizeRequireOps(meta.requireOps) return route } permissionRoutes = routeMap(permissionRoutes, normalizeRouteMeta) const permissionRoutesCopy = JSON.parse(JSON.stringify(permissionRoutes))
獲取到操作列表后,只需要遍歷權(quán)限路由,然后查詢 requireOps 代表的操作有沒(méi)有在操作列表中。這里需要處理一下 requireOps 未設(shè)置的情況,如果子路由中都是權(quán)限路由,需要為父級(jí)路由自動(dòng)加上 requireOps 值,不然當(dāng)所有子路由都沒(méi)有權(quán)限時(shí),父級(jí)路由就被認(rèn)為是無(wú)權(quán)限控制且可訪問(wèn)的;而如果子路由中只要有一個(gè)路由無(wú)權(quán)限控制,那就不需要處理父路由。所以這里可以用遞歸來(lái)解決,先處理子路由再處理父路由
const filterPermissionRoutes = (routes, cb) => { // 可能父路由沒(méi)有設(shè)置requireOps 需要根據(jù)子路由確定父路由的requireOps routes.forEach(route => { if (route.children) { route.children = filterPermissionRoutes(route.children, cb) if (!route.meta.requireOps) { const hasNoPermission = route.children.some(child => child.meta.requireOps === null) // 如果子路由中存在不需要權(quán)限控制的路由,則跳過(guò) if (!hasNoPermission) { route.meta.requireOps = [].concat(...route.children.map(child => child.meta.requireOps)) } } } }) return cb(routes) }
然后根據(jù)操作列表對(duì)權(quán)限路由進(jìn)行過(guò)濾
let operations = null // 從后端獲取后更新它 const hasOp = opcode => operations ? operations.some(op => op.opcode === opcode) : false const proutes = filterPermissionRoutes(permissionRoutes, routes => routes.filter(route => { const requireOps = route.meta.requireOps if (requireOps) { return requireOps.some(hasOp) } return true })) // 動(dòng)態(tài)添加路由 router.addRoutes(proutes)
函數(shù)式組件控制局部權(quán)限
這個(gè)組件實(shí)現(xiàn)很簡(jiǎn)單,根據(jù)傳入的操作碼進(jìn)行權(quán)限判斷,若通過(guò)則返回插槽內(nèi)容,否則返回null。另外,為了統(tǒng)一風(fēng)格,支持一下 root 屬性,表示組件的根節(jié)點(diǎn)
const AccessControl = { functional: true, render (h, { data, children }) { const attrs = data.attrs || {} // 如果是root,直接透?jìng)? if (attrs.root !== undefined) { return h(attrs.root || 'div', data, children) } if (!attrs.opcode) { return h('span', { style: { color: 'red', fontSize: '30px' } }, '請(qǐng)配置操作碼') } const opcodes = attrs.opcode.split(',') if (opcodes.some(hasOp)) { return children } return null } }
動(dòng)態(tài)生成權(quán)限菜單
以ElementUI為例,由于動(dòng)態(tài)渲染需要進(jìn)行遞歸,如果以文件組件的形式會(huì)多一層根組件,所以這里直接用render function簡(jiǎn)單寫一個(gè)示例,可以根據(jù)自己的需求改造
// 權(quán)限菜單組件 export const PermissionMenuTree = { name: 'MenuTree', props: { routes: { type: Array, required: true }, collapse: Boolean }, render (h) { const createMenuTree = (routes, parentPath = '') => routes.map(route => { // hidden: 為true時(shí)當(dāng)前菜單和子菜單都不顯示 if (route.hidden === true) { return null } // 子路徑處理 const fullPath = route.path.charAt(0) === '/' ? route.path : `${parentPath}/${route.path}` // visible: 為false時(shí)不顯示當(dāng)前菜單,但顯示子菜單 if (route.visible === false) { return createMenuTree(route.children, fullPath) } const title = route.meta.title const props = { index: fullPath, key: route.path } if (!route.children || route.children.length === 0) { return h( 'el-menu-item', { props }, [h('span', title)] ) } return h( 'el-submenu', { props }, [ h('span', { slot: 'title' }, title), ...createMenuTree(route.children, fullPath) ] ) }) return h( 'el-menu', { props: { collapse: this.collapse, router: true, defaultActive: this.$route.path } }, createMenuTree(this.routes) ) } }
接口的權(quán)限控制
我們一般用axios,這里只需要在axios封裝的基礎(chǔ)上加幾行代碼就可以了,axios封裝花樣多多,這里簡(jiǎn)單示例
const ajax = axios.create(/* config */) export default { post (url, data, opcode, config = {}) { if (opcode && !hasOp(opcode)) { return Promise.reject(new Error('沒(méi)有操作權(quán)限')) } return ajax.post(url, data, { /* config */ ...config }).then(({ data }) => data) }, // ... }
到這里,這個(gè)方案差不多就完成了,權(quán)限配置的可視化可以根據(jù)操作列表中的 routeName
來(lái)做,將操作與權(quán)限路由一一對(duì)應(yīng),在 demo 中有一個(gè)簡(jiǎn)單實(shí)現(xiàn)
總結(jié)
以上所述是小編給大家介紹的Vue實(shí)現(xiàn)按鈕級(jí)權(quán)限方案,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
如果你覺(jué)得本文對(duì)你有幫助,歡迎轉(zhuǎn)載,煩請(qǐng)注明出處,謝謝!
- Vue自定義指令實(shí)現(xiàn)按鈕級(jí)的權(quán)限控制的示例代碼
- vue3實(shí)現(xiàn)按鈕權(quán)限管理的項(xiàng)目實(shí)踐
- vue3?自定義指令控制按鈕權(quán)限的操作代碼
- vue實(shí)現(xiàn)前端按鈕組件權(quán)限管理
- vue使用自定義指令來(lái)控制頁(yè)面按鈕組的權(quán)限思想
- vue使用自定義指令實(shí)現(xiàn)按鈕權(quán)限展示功能
- vue路由權(quán)限和按鈕權(quán)限的實(shí)現(xiàn)示例
- vue 按鈕 權(quán)限控制介紹
- Vue按鈕權(quán)限的實(shí)現(xiàn)示例
相關(guān)文章
go-gin-vue3-elementPlus帶參手動(dòng)上傳文件的案例代碼
這篇文章主要介紹了go-gin-vue3-elementPlus帶參手動(dòng)上傳文件的案例代碼,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-11-11Vue 第三方字體圖標(biāo)引入 Font Awesome的方法
今天小編就為大家分享一篇Vue 第三方字體圖標(biāo)引入 Font Awesome的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09vue與bootstrap實(shí)現(xiàn)時(shí)間選擇器的示例代碼
本篇文章主要介紹了vue與bootstrap實(shí)現(xiàn)時(shí)間選擇器的示例代碼,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-08-08Vue3中使用Element-Plus的el-upload組件限制只上傳一個(gè)文件的功能實(shí)現(xiàn)
在 Vue 3 中使用 Element-Plus 的 el-upload 組件進(jìn)行文件上傳時(shí),有時(shí)候需要限制只能上傳一個(gè)文件,本文將介紹如何通過(guò)配置 el-upload 組件實(shí)現(xiàn)這個(gè)功能,讓你的文件上傳變得更加簡(jiǎn)潔和易用,需要的朋友可以參考下2023-10-10vue element table 表格請(qǐng)求后臺(tái)排序的方法
今天小編就為大家分享一篇vue element table 表格請(qǐng)求后臺(tái)排序的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09