原文作者: 雅X共賞
Vue-Access-Control是一套基於Vue/Vue-Router/axios 實現的前端用戶權限控制解決方案,通過對路由、視圖、請求三個層面的控制,使開發者可以實現任意顆粒度的用戶權限控制。
安裝
版本要求
- Vue 2.0x
- Vue-router 3.x
獲取
項目主頁:https//refined-x.com/Vue-Access-Control/
git:git clone https://github.com/tower1229/Vue-Access-Control.git
運行
1 |
//開發 |
概述
整體思路
會話開始之初,先初始化一個只有登錄路由的Vue實例,在根組件created鈎子里將路由定向到登錄頁,用戶登錄成功后前端拿到用戶token,設置axios實例統一為請求headers添加{"Authorization":token}
實現用戶鑒權,然后獲取當前用戶的權限數據,主要包括路由權限和資源權限,之后動態添加路由,生成菜單,實現權限指令和全局權限驗證方法,並為axios實例添加請求攔截器,至此完成權限控制初始化。動態加載路由后,路由組件將隨之加載並渲染,而后展現前端界面。
為解決瀏覽器刷新路由重置的問題,拿到token后要將其保存到sessionStorage
,根組件的created鈎子負責檢查本地是否已有token,如果有則無需登錄直接用該token獲取權限並初始化,如果token有效且當前路由有權訪問,將加載路由組件並正確展現;若當前路由無權訪問將按路由設置跳轉404;如果token失效,后端應返回4xx狀態碼,前端統一為axios實例添加錯誤攔截器,遇到4xx狀態碼執行退出操作,清除sessionStorage
數據並跳轉到登錄頁,讓用戶重新登錄。
最小依賴原則
Vue-Access-Control的定位是單一領域解決方案,除了Vue/Vue-Router/axios之外沒有其他依賴,理論上可以無障礙的應用到任何有權限控制需求的Vue項目中,項目基於webpack 模板開發構建,大多數新項目可以直接基於檢出代碼繼續開發。需要說明的是,項目額外引入的Element-UI和CryptoJS僅用於開發演示界面,他們不是必須且與權限控制毫無關系,項目應用中可以自行取舍。
目錄結構
1 |
src/ |
數據格式約定
-
路由權限數據必須是如下格式的對象數組,
id
和parent_id
相同的兩個路由具有上下級關系,如果希望使用自定義格式的路由數據,需要修改路由控制的相關實現,詳見路由控制1
2
3
4
5
6
7
8
9
10
11
12
13
14[
{
"id": "1",
"name": "菜單1",
"parent_id": null,
"route": "route1"
},
{
"id": "2",
"name": "菜單1-1",
"parent_id": "1",
"route": "route2"
}
] -
資源權限數據必須是如下格式的對象數組,每個對象代表一個RESTful請求,支持帶參數的url,具體格式說明見請求控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14[
{
"id": "2c9180895e172348015e1740805d000d",
"name": "賬號-獲取",
"url": "/accounts",
"method": "GET"
},
{
"id": "2c9180895e172348015e1740c30f000e",
"name": "賬號-刪除",
"url": "/account/**",
"method": "DELETE"
}
]
路由控制
路由控制包括動態注冊路由和動態生成菜單兩部分。
動態注冊路由
最初實例化的路由僅包括登錄和404兩個路徑,我們期待完整的路由是這樣的:
1 |
[{ |
那么接下來就需要獲取首頁以及其子路由們,思路是事先在本地存一份整個項目的完整路由數據,然后根據用戶權限對完整路由進行篩選。
篩選的實現思路是先將后端返回的路由數據處理成如下哈希結構:
1 |
let hashMenus = { |
然后遍歷本地完整路由,在循環中將路徑拼接成上述結構中的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
或自定義指令就能靈活實現各種視圖控制。
全局驗證方法
驗證方法的的實現本身很簡單,無非是根據后端給出的資源權限做判斷,重點在於優化方法的輸入輸出,提升易用性,經過實踐總結最終使用的方案是,將權限跟請求同時維護,驗證方法接收請求對象數組為參數,返回是否具有權限的布爾值。
請求對象格式:
1 |
//獲取賬戶列表 |
權限驗證方法$_has()
的調用格式:
1 |
v-if="$_has([request])" |
權限驗證方法的具體實現見App.vue
中Vue.prototype.$_has
方法。
將權限驗證方法全局混入,就可以在項目中很容易的配合v-if
實現元素顯示控制,這種方式的優點在於靈活,除了可以校驗權限外,還可以在判斷表達式中加入運行時狀態做更多樣性的判斷,而且可以充分利用v-if
響應數據變化的特點,實現動態視圖控制。
具體實現細節參考基於Vue實現后台系統權限控制中的相關章節。
自定義指令
v-if
的響應特性是把雙刃劍,因為判斷表達式在運行過程中會頻繁觸發,但實際上在一個用戶會話周期內其權限並不會發生變化,因此如果只需要校驗權限的話,用v-if
會產生大量不必要的運算,這種情況只需在視圖載入時校驗一次即可,可以通過自定義指令實現:
1 |
//權限指令 |
自定義指令內部仍然是調用全局驗證方法,但優點在於只會在元素初始化時執行一次,多數情況下都應該使用自定義指令實現視圖控制。
請求控制
請求控制是利用axios攔截器實現的,目的是將越權請求在前端攔截掉,原理是在請求攔截器中判斷本次請求是否符合用戶權限,以決定是否攔截。
普通請求的判斷很容易,遍歷后端返回的的資源權限格式,直接判斷request.method
和request.url
是否吻合就可以了,對於帶參數的url需要使用通配符,這里需要根據項目需求前后端協商一致,約定好通配符格式后,攔截器中要先將帶參數的url處理成約定格式,再判斷權限,方案中已經實現了以下兩種通配符格式:
1 |
1. 格式:/resources/:id |
對於第一種格式需要注意的是,如果你要發起一個url為"/aaa/bbb"
的請求,默認會被處理成"/aaa/**"
進行權限校驗,如果這里的”bbb”並不是參數而是url的一部分,那么你需要將url改成"/aaa/bbb/"
,在最后加一個”/“表示該url不需要轉化格式。
攔截器的具體實現見App.vue
中的setInterceptor()
方法。
如果你的項目還需要其他的通配符格式,只需要在攔截器中實現對應的檢測和轉化方法就可以了。