基於Vue實現後臺系統權限控制

原文地址:http://refined-x.com/2017/08/29/基於Vue實現後臺系統權限控制/,轉載請註明出處。html

 

用Vue這類雙向綁定框架作後臺系統再適合不過,後臺系統相比普通前端項目除了數據交互更頻繁之外,還有一個特別的需求就是對用戶的權限控制,那麼如何在一個Vue應用中實現權限控制呢?下面是個人一點經驗。前端

權限控制是什麼

在權限的世界裏服務端提供的一切都是資源,資源能夠由請求方法+請求地址來描述,權限是對特定資源的訪問許可,所謂權限控制,也就是確保用戶只能訪問到被分配的資源。具體的說,前端對資源的訪問一般是由界面上的按鈕發起,好比刪除某條數據;或由用戶進入某一個頁面發起,好比獲取某個列表數據。這兩種形式覆蓋了資源請求的大部分場景,所以權限控制也能夠被籠統的分紅菜單權限控制和按鈕權限控制。ios

Vue菜單權限控制

菜單是對路由的直接體現,菜單控制實際上就是路由控制。實現路由控制一個簡單的方式是,在路由的before鉤子裏校驗當前即將跳轉的路由地址是否有權訪問,根據校驗結果決定路由是否放行,僞碼:git

1
2
3
4
5
6
7
8
router.beforeEach((to, from, next) => {
//權限校驗
let pass = valid(to);
if(!pass){
return console.log('無權訪問');
}
next();
});

這種實現方式既簡單又直觀,用於簡單的系統很是合適,但這麼作本質上是將全部路由所有註冊了,直接帶來的缺點有兩個:1、若是路由組件不是按需加載的話,應用將加載大量冗餘代碼;2、每次跳轉都要遍歷一次完整路由是對計算能力的浪費,對於路由總數較大的應用很不可取。github

理想的實現方式是本地保存完整路由,但並不當即初始化Vue應用,待用戶登陸拿到權限後,用菜單權限篩選出可用路由,再用可用路由初始化Vue應用。也就是說,要將登陸頁獨立出去作成一個單獨的頁面,登陸後將用戶數據保存在本地,再經過url跳轉到Vue應用所在頁面,Vue應用啓動前經過本地用戶數據完成路由篩選,而後初始化Vue應用,僞碼以下:element-ui

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//main.js
let user = sessionStorage.getItem('user');
if (user) {
user = JSON.parse(user);
//篩選獲得實際路由
let fullPath = require('fullPath.js');
let routes = filter(fullPath, user.menus);
//建立路由對象
let router = new Router({routes});
//生成Vue實例
new Vue({
el: '#app',
router,
render: h => h(App)
});
}else{
location.href = '/login/';
}

這樣咱們就根據用戶權限生成了一套」定製」路由,這時咱們還但願能直接用路由生成導航菜單,常規的路由數據可能沒法知足菜單組件的需求,因此咱們能夠事先在路由的meta數據裏維護上菜單數據,好比菜單名稱菜單圖標等,只要在模板中經過$router.options就能夠訪問到當前路由數據,若是使用element-ui的菜單組件實現,代碼大體是這樣的:axios

1
2
3
4
5
6
7
<el-menu router>
<el-menu-item v-for="(route, index) in $router.options.routes[2].children"
:route="route"
:index="route.name">
<i class="ion" v-html="route.icon"></i>{{route.name}}
</el-menu-item>
</el-menu>

固然這樣只能循環出一級菜單,若是還有二級路由須要對應二級菜單的話,就得判斷並循環children節點,比較簡單就不放更多代碼了,菜單權限控制到這裏就完成了。api

Vue按鈕權限控制

按鈕權限控制與菜單權限控制的實現思路相似,也是根據用戶權限判斷各個按鈕的顯示與否,方式無非是v-if或自定義指令,並且只要將v-if背後的權限校驗邏輯抽象成方法,不管是代碼量仍是使用形式上都跟自定義指令幾乎同樣,但v-if的特色是它會響應數據變化,所以隨着應用的運行會頻繁觸發權限校驗,而權限在應用的整個生命週期內其實只需校驗一次,爲了不無謂的程序執行,這裏能夠用自定義指令來實現,僞碼:數組

1
2
3
4
5
6
7
8
9
10
Vue.directive('has', {
bind: function (el, binding) {
if(!has(binding.value)){
el.parentNode.removeChild(el);
}
}
});

//用法:
<btn v-has='get,/sources'>按鈕</btn>

注意在指令bind回調裏有一個has()方法,這就是權限校驗方法,咱們同時將這個方法全局混合到Vue對象中,使應用裏的每一個組件均可以訪問到這個方法,便於爲界面上的v-if提供支持,例如:session

1
2
3
<div v-if="has('get,/sources') && something">
一個須要同時具有'get,/sources'權限和somthing爲真值才顯示的div
</div>

這樣一來凡是須要依據權限實現的按鈕顯隱控制和界面變化均可以很方便的實現。

但按鈕權限控制真正麻煩的地方不在於如何實現,而在於高昂的維護成本。咱們假設按鈕Btn綁定了點擊回調Fn,回調Fn裏發起了請求Req,請求Req須要某個資源的訪問權限,最終你要根據用戶是否擁有Req的權限,決定Btn是否顯示,而Req跟Btn之間並無直接關聯,因此咱們就要人肉維護他們的關係,一個複雜項目裏的按鈕有個幾十上百都很正常,隨着業務的變動去維護這麼多按鈕的權限,想一想都頭疼。

有一個方法能夠繞開這個爛攤子,那就是前端放棄對視圖層的控制,退到請求層面,在請求發起前集中攔截,這時能夠直接根據請求方法和請求地址來校驗權限,除了實現一個攔截器以外不須要額外的代碼,能夠說很是優雅了。以axios爲例,攔截器大概長這樣:

1
2
3
4
5
6
7
8
9
axios.interceptors.request.use(function (config) {
if(!has(config)){
//驗證不經過
return Promise.reject({
message: `no permission`
});
}
return config;
});

但若是僅僅這樣作權限控制,界面上將顯示出全部的按鈕,用戶看到的按鈕卻不必定能夠點擊,這種體驗我認爲只能停留在理論層面,根本沒法應用到實際產品中。請求控制能夠做爲整個控制體系的第二道防線,或某些特殊狀況下的輔助手段,最終仍是要回到按鈕控制的思路上來。

那麼怎樣能儘量方便的採集到每一個按鈕所需的權限呢?按鈕和權限之間隔着兩層東西,第一層是click回調,第二層是回調裏的AJAX請求,不想人肉維護就得想辦法突破這兩層隔閡,讓按鈕和權限產生聯繫,按鈕必然要綁定click事件,最理想的採集方式是在綁定事件的同時獲得所需權限,讓一切天然而然的發生,好比這樣,

1
<btn v-do="Fn">按鈕</btn>

若是Fn能以某種形式採集到內部的AJAX請求參數,並轉化成權限信息傳遞出來就完美了,然而我沒找到可行的方法,而且這種形式在應用上也存在缺陷,由於不必定每一個操做按鈕都會發起AJAX請求,好比編輯按鈕自己並不會觸發請求,真正觸發請求的是另外一個保存按鈕,因此這個思路只是看起來很美。

那麼退而求其次的作法是讓按鈕和請求聯繫起來,好比說按鈕涉及一個名稱爲A的請求,那麼我但願權限指令能夠這樣寫,

1
<btn v-has="A" @click="Fn">按鈕</btn>

比完美形態是差了很多,但起碼不須要手動維護到'get,/resources'這個級別了,這裏對A的實現能夠有多種形式,好比A能夠是一個包含兩個屬性的對象:

1
2
3
4
5
6
7
8
9
10
11
12
13
const A = {
p: ['put,/menu/**'],
r: params => {
return axios.put(`/menu/${params.id}`, params)
}
};

//用做權限:
<btn v-has="[A]" @click="Fn">按鈕</btn>
//用做請求:
function Fn(){
A.r().then((res) => {})
}

一般咱們會將項目裏全部的api放在一個api模塊裏集中管理,在寫api時順便就把權限給維護了,換來的是在組件界面裏能夠直接用請求名稱來描述權限,而不須要來回奔波於界面和api模塊之間,必定程度上實現了關注點分離,並且has指令還能夠進一步作優化,例如參數只須要接收A,指令內部根據約定自動訪問A.p來獲取權限,還能夠接收數組,容許多個權限聯合校驗。

後記

好了,這就是我對前端權限控制的一些實踐和思考,若有不當歡迎指正。

上述方案已開源,項目地址:Vue-Access-Control

相關文章
相關標籤/搜索