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