Vue-Access-Control是一套基於Vue/Vue-Router/axios 實現的前端用戶權限控制解決方案,經過對路由、視圖、請求三個層面的控制,使開發者能夠實現任意顆粒度的用戶權限控制。前端
會話開始之初,先初始化一個只有登陸路由的Vue實例,在根組件created鉤子裏將路由定向到登陸頁,用戶登陸成功後前端拿到用戶token,設置axios實例統一爲請求headers添加{"Authorization":token}
實現用戶鑑權,而後獲取當前用戶的權限數據,主要包括路由權限和資源權限,以後動態添加路由,生成菜單,實現權限指令和全局權限驗證方法,併爲axios實例添加請求攔截器,至此完成權限控制初始化。動態加載路由後,路由組件將隨之加載並渲染,然後展示前端界面。vue
爲解決瀏覽器刷新路由重置的問題,拿到token後要將其保存到sessionStorage
,根組件的created鉤子負責檢查本地是否已有token,若是有則無需登陸直接用該token獲取權限並初始化,若是token有效且當前路由有權訪問,將加載路由組件並正確展示;若當前路由無權訪問將按路由設置跳轉404;若是token失效,後端應返回4xx狀態碼,前端統一爲axios實例添加錯誤攔截器,遇到4xx狀態碼執行退出操做,清除sessionStorage
數據並跳轉到登陸頁,讓用戶從新登陸。webpack
Vue-Access-Control的定位是單一領域解決方案,除了Vue/Vue-Router/axios以外沒有其餘依賴,理論上能夠無障礙的應用到任何有權限控制需求的Vue項目中,項目基於webpack 模板開發構建,大多數新項目能夠直接基於檢出代碼繼續開發。須要說明的是,項目額外引入的Element-UI和CryptoJS僅用於開發演示界面,他們不是必須且與權限控制毫無關係,項目應用中能夠自行取捨。ios
src/ |-- api/ //接口文件 | |-- index.js //輸出通用axios實例 | |-- account.js //按業務模塊組織的接口文件,全部接口都引用./index提供的axios實例 |-- assets/ |-- components/ |-- router/ | |-- fullpath.js //完整路由數據,用於匹配用戶的路由權限獲得實際路由 | `-- index.js //輸出基礎路由實例 |-- views/ |-- App.vue ·-- main.js
路由權限數據必須是以下格式的對象數組,id
和parent_id
相同的兩個路由具備上下級關係,若是但願使用自定義格式的路由數據,須要修改路由控制的相關實現,詳見路由控制數據格式約定git
[ { "id": "1", "name": "菜單1", "parent_id": null, "route": "route1" }, { "id": "2", "name": "菜單1-1", "parent_id": "1", "route": "route2" } ]
資源權限數據必須是以下格式的對象數組,每一個對象表明一個RESTful請求,支持帶參數的url,具體格式說明見請求控制github
[ { "id": "2c9180895e172348015e1740805d000d", "name": "帳號-獲取", "url": "/accounts", "method": "GET" }, { "id": "2c9180895e172348015e1740c30f000e", "name": "帳號-刪除", "url": "/account/**", "method": "DELETE" } ]
路由控制包括動態註冊路由和動態生成菜單兩部分。web
最初實例化的路由僅包括登陸和404兩個路徑,咱們期待完整的路由是這樣的:npm
[{ path: '/login', name: 'login', component: (resolve) => require(['../views/login.vue'], resolve) }, { path: '/404', name: '404', component: (resolve) => require(['../views/common/404.vue'], resolve) }, { path: '/', name: '首頁', component: (resolve) => require(['../views/index.vue'], resolve), children: [{ path: '/route1', name: '欄目1', meta: { icon: 'icon-channel1' }, component: (resolve) => require(['../views/view1.vue'], resolve) }, { path: '/route2', name: '欄目2', meta: { icon: 'ico-channel2' }, component: (resolve) => require(['../views/view2.vue'], resolve), children: [{ path: 'child2-1', name: '子欄目2-1', meta: { }, component: (resolve) => require(['../views/route2-1.vue'], resolve) }] }] }, { path: '*', redirect: '/404' }]
那麼接下來就須要獲取首頁以及其子路由們,思路是事先在本地存一份整個項目的完整路由數據,而後根據用戶權限對完整路由進行篩選。編程
篩選的實現思路是先將後端返回的路由數據處理成以下哈希結構:axios
let hashMenus = { "/route1":true, "/route1/route1-1":true, "/route1/route1-2":true, "/route2":true, ... }
而後遍歷本地完整路由,在循環中將路徑拼接成上述結構中的key格式,經過hashMenus[route]
就能夠判斷路由是否匹配,具體實現見App.vue
文件中的getRoutes()
方法。
若是後端返回的路由權限數據與約定不一樣,就須要自行實現篩選邏輯,只要能獲得實際可用的路由數據就能夠,最終使用addRoutes()
方法將他們動態添加到路由實例中,注意404頁面的模糊匹配必定要放在最後。
路由數據能夠直接用來生成導航菜單,但路由數據是在根組件中獲得的,導航菜單存在於index.vue
組件中,顯然咱們須要經過某種方式共享菜單數據,方法有不少,通常來講首先想到的是Vuex,但菜單數據在整個用戶會話過程當中不會發生改變,這並非Vuex的最佳使用場景,並且爲了儘可能減小沒必要要的依賴,這裏用了最簡單直接的方法,把菜單數據掛在根組件data.menuData
上,在首頁裏用this.$parent.menuData
獲取。
另外,導航菜單極可能會有添加欄目圖標的需求,這能夠經過在路由中添加meta
數據實現,例如將圖標class或unicode存到路由meta裏,模板中就能夠訪問到meta數據,用來生成圖標標籤。
在多角色系統中可能遇到的一個問題是,不一樣角色有一個名字相同但功能不一樣的路由,好比說系統管理員和企業管理員都有」帳號管理」這個路由,但他們的操做權限和目標不一樣,其實是兩個徹底不一樣的界面,而Vue不容許多個路由同名,所以路由的name必須作區分,但把區分後的name顯示在前端菜單上會很不美觀,爲了讓不一樣角色能夠享有同一個菜單名稱,咱們只要將這兩個路由的meta.name
都設置成」帳號管理」,在模板循環時優先使用meta.name
就能夠了。
菜單的具體實現能夠參考views/index.vue
。
視圖控制的目標是根據當前用戶權限決定界面元素顯示與否,典型場景是對各類操做按鈕的顯示控制。實現視圖控制的本質是實現一個權限驗證方法,輸入請求權限,輸出是否獲准。而後配合v-if
或jsx
或自定義指令就能靈活實現各類視圖控制。
驗證方法的的實現自己很簡單,無非是根據後端給出的資源權限作判斷,重點在於優化方法的輸入輸出,提高易用性,通過實踐總結最終使用的方案是,將權限跟請求同時維護,驗證方法接收請求對象數組爲參數,返回是否具備權限的布爾值。
請求對象格式:
//獲取帳戶列表 const request = { p: ['get,/accounts'], r: params => { return instance.get(`/accounts`, {params}) } }
權限驗證方法$_has()
的調用格式:
v-if="$_has([request])"
權限驗證方法的具體實現見App.vue
中Vue.prototype.$_has
方法。
將權限驗證方法全局混入,就能夠在項目中很容易的配合v-if
實現元素顯示控制,這種方式的優勢在於靈活,除了能夠校驗權限外,還能夠在判斷表達式中加入運行時狀態作更多樣性的判斷,並且能夠充分利用v-if
響應數據變化的特色,實現動態視圖控制。
具體實現細節參考基於Vue實現後臺系統權限控制中的相關章節。
v-if
的響應特性是把雙刃劍,由於判斷表達式在運行過程當中會頻繁觸發,但實際上在一個用戶會話週期內其權限並不會發生變化,所以若是隻須要校驗權限的話,用v-if
會產生大量沒必要要的運算,這種狀況只需在視圖載入時校驗一次便可,能夠經過自定義指令實現:
//權限指令 Vue.directive('has', { bind: function(el, binding) { if (!Vue.prototype.$_has(binding.value)) { el.parentNode.removeChild(el); } } });
自定義指令內部仍然是調用全局驗證方法,但優勢在於只會在元素初始化時執行一次,多數狀況下都應該使用自定義指令實現視圖控制。
請求控制是利用axios攔截器實現的,目的是將越權請求在前端攔截掉,原理是在請求攔截器中判斷本次請求是否符合用戶權限,以決定是否攔截。
普通請求的判斷很容易,遍歷後端返回的的資源權限格式,直接判斷request.method
和request.url
是否吻合就能夠了,對於帶參數的url須要使用通配符,這裏須要根據項目需求先後端協商一致,約定好通配符格式後,攔截器中要先將帶參數的url處理成約定格式,再判斷權限,方案中已經實現瞭如下兩種通配符格式:
1. 格式:/resources/:id 示例:/resources/1 url: /resources/** 解釋:一個名詞後跟一個參數,參數一般表示名詞的id 2. 格式:/store/:id/member 示例:/store/1/member url:/store/*/member 解釋:兩個名詞之間夾帶一個參數,參數一般表示第一個名詞的id
對於第一種格式須要注意的是,若是你要發起一個url爲"/aaa/bbb"
的請求,默認會被處理成"/aaa/**"
進行權限校驗,若是這裏的」bbb」並非參數而是url的一部分,那麼你須要將url改爲"/aaa/bbb/"
,在最後加一個」/「表示該url不須要轉化格式。
攔截器的具體實現見App.vue
中的setInterceptor()
方法。
若是你的項目還須要其餘的通配符格式,只須要在攔截器中實現對應的檢測和轉化方法就能夠了。
DEMO項目中演示了動態菜單、動態路由、按鈕權限、請求攔截。
演示項目後端由rap2生成mock數據,登陸請求一般應該是POST方式,但由於rap2的編程模式沒法獲取到非GET的請求參數,所以只能用GET方式登陸,實際項目中不建議仿效;
另外登陸後獲取權限的接口原本不須要攜帶額外參數,後端能夠根據請求頭攜帶的token信息實現用戶鑑權,但由於rap2的編程模式獲取不到headers數據,所以只能增長一個」Authorization」參數用於生成模擬數據。
1. username: root password: 任意 2. username: client password: 任意