#?Vue項目前后端分離下的前端鑒權(quán)方案
###?技術(shù)棧
??前端Vue全家桶涡相,后臺.net欢峰。
###?需求分析
??1.?前端路由鑒權(quán)炊邦,屏蔽地址欄入侵
??2.?路由數(shù)據(jù)由后臺管理芙粱,前端只按固定規(guī)則異步加載路由
??3.?權(quán)限控制精確到每一個按鈕
??4.?自動更新token
??5.?同一個瀏覽器只能登錄一個賬號
###?前端方案
>?對于需求1划咐、2拴念、3,采用異步加載路由方案
??1.?首先編寫vue全局路由守衛(wèi)
??2.?排除登錄路由和無需鑒權(quán)路由
??3.?登錄后請求拉取用戶菜單數(shù)據(jù)
??4.?在vuex里處理菜單和路由匹配數(shù)據(jù)
??5.?將在vuex里處理好的路由數(shù)據(jù)通過`addRoutes`異步推入路由
??```
????router.beforeEach((to,?from,?next)?=>?{
??????//?判斷當(dāng)前用戶是否已拉取權(quán)限菜單
??????if?(store.state.sidebar.userRouter.length?===?0)?{
????????//?無菜單時拉取
????????getMenuRouter()
??????????.then(res?=>?{
????????????let?_menu?=?res.data.Data.ColumnDataList?||?[];
????????????//?if?(res.data.Data.ColumnDataList.length?>?0)?{
????????????//?整理菜單&路由數(shù)據(jù)
????????????store.commit("setMenuRouter",?_menu);
????????????//?推入權(quán)限路由列表
????????????router.addRoutes(store.state.sidebar.userRouter);
????????????next({...to,?replace:?true?});
????????????//?}
??????????})
??????????.catch(err?=>?{
????????????//?console.log(err);
????????????//?Message.error("服務(wù)器連接失敗");
??????????});
??????}?else?{
????????//當(dāng)有用戶權(quán)限的時候褐缠,說明所有可訪問路由已生成?如訪問沒權(quán)限的菜單會自動進入404頁面
????????if?(to.path?==?"/login")?{
??????????next({
????????????name:?"index"
??????????});
????????}?else?{
??????????next();
????????}
??????}
????}?else?{
??????//?無登錄狀態(tài)時重定向至登錄?或可進入無需登錄狀態(tài)路徑
??????if?(to.path?==?"/login"?||?to.meta.auth?===?0)?{
????????next();
??????}?else?{
????????next({
??????????path:?"/login"
????????});
??????}
????}
??});
??```
??#####?注意
??>?我這里無需鑒權(quán)的路由直接寫在router文件夾下的index.js政鼠,通過路由元信息meta攜帶指定標(biāo)識
??```
????{
??????path:?"/err-404",
??????name:?"err404",
??????meta:?{
?????????authentication:?false
??????},
??????component:?resolve?=>?require(["../views/error/404.vue"],?resolve)
????},
??```
??>?上面說到路由是根據(jù)后臺返回菜單數(shù)據(jù)根據(jù)一定規(guī)則生成,因此一些不是菜單队魏,又需要登錄狀態(tài)的路由公般,我寫在router文件夾下的router.js里,在上面步驟4里處理后臺返回菜單數(shù)據(jù)時器躏,和處理好的菜單路由數(shù)據(jù)合并一同通過`addRoutes`推入俐载。?
??這樣做會有一定的被地址欄入侵的風(fēng)險,但是筆者這里大多是不太重要的路由,如果你要求咳咳登失,可以定一份字典來和后臺接口配合精確加載每一個路由遏佣。
??```
??//?加入企業(yè)
??{
????path:?"/join-company",
????name:?"join-company",
????component:?resolve?=>?require([`@/views/index/join-company.vue`],?resolve)?
??},
??```
??>?在vuex中將分配的菜單數(shù)據(jù)轉(zhuǎn)化為前端可用的路由數(shù)據(jù),我是這樣做的:
??管理系統(tǒng)在新增菜單時需要填寫一個頁面地址字段`Url`,前端得到后臺菜單數(shù)據(jù)后根據(jù)`Url`字段來匹配路由加載的文件路徑揽浙,每個菜單一個文件夾的好處是:你可以在這里拆分js状婶、css和此菜單私有組件等
??```
??????menu.forEach(item?=>?{
????????????let?routerItem?=?{
??????????????path:?item.Url,
??????????????name:?item.Id,
??????????????meta:?{
????????????????auth:?item.Children,
??????????????},?//?路由元信息?定義路由時即可攜帶的參數(shù),可用來管理每個路由的按鈕操作權(quán)限
??????????????component:?resolve?=>
????????????????require([`@/views${item.Url}/index.vue`],?resolve)?//?路由映射真實視圖路徑
????????????};
????????????routerBox.push(routerItem);
????????});
??```
??>?關(guān)于如何精確控制每一個按鈕我是這樣做的馅巷,將按鈕編碼放在路由元信息里膛虫,在當(dāng)前路由下匹配來控制頁面上的按鈕是否創(chuàng)建。
??菜單數(shù)據(jù)返回的都是多級結(jié)構(gòu)钓猬,每個菜單下的子集就是當(dāng)前菜單下的按鈕權(quán)限碼數(shù)組稍刀,我把每個菜單下的按鈕放在此菜單的路由元信息`meta.auth`中。這樣作的好處是:按鈕權(quán)限校驗只需匹配每個菜單路由元信息下的數(shù)據(jù)敞曹,這樣校驗池長度通常不會超過5個账月。
??```
??created()?{
????this.owner?=?this.$route.meta.auth.map(item?=>?item.Code);
??}
??methods:?{
??????matchingOwner(auth)?{
????????return?this.owner.some(item?=>?item?===?auth);
??????}
??}
??```
??>?需求4自動更新token,就是簡單的時間判斷澳迫,并在請求頭添加字段來通知后臺更新token并在頭部返回局齿,前端接受到帶token的請求就直接更新token
??```
??//?在axios的請求攔截器中
??????let?token?=?getSession(auth_code);
??????if?(token)?config.headers.auth?=?token;
??????if?(tokenIsExpire(token))?{
????????//?判斷是否需要刷新jwt
????????config.headers.refreshtoken?=?true;
??????}
??//?在axios的響應(yīng)攔截器中
????if?(res.headers.auth)?{
??????setSession(auth_code,?res.headers.auth);
????}
??```
??>?對于需求5的處理比較麻煩,要跨tab頁只能通過`cookie`或`local`,筆者這里不允許使用`cookie`因此采用的`localstorage`橄登。通過打開的新頁面讀取`localstorage`內(nèi)的`token`數(shù)據(jù)來同步多個頁面的賬號信息抓歼。`token`使用的`jwt`并前端md5加密讥此。
??這里需要注意一點是頁面切換要立即同步賬號信息。
??>?經(jīng)過需求5改造后的全局路由守衛(wèi)是這樣的:
??```
function?_AUTH_()?{
??//?切換窗口時校驗賬號是否發(fā)生變化
??window.addEventListener("visibilitychange",?function()?{
????let?Local_auth?=?getLocal(auth_code,?true);
????let?Session_auth?=?getSession(auth_code);
????if?(document.hidden?==?false?&&?Local_auth?&&?Local_auth?!=?Session_auth)?{
??????setSession(auth_code,?Local_auth,?true);
??????router.go(0)
????}
??})
??router.beforeEach((to,?from,?next)?=>?{
??????//?判斷當(dāng)前用戶是否已拉取權(quán)限菜單
??????if?(store.state.sidebar.userRouter.length?===?0)?{
????????//?無菜單時拉取
????????getMenuRouter()
??????????.then(res?=>?{
????????????let?_menu?=?res.data.Data.ColumnDataList?||?[];
????????????//?if?(res.data.Data.ColumnDataList.length?>?0)?{
????????????//?整理菜單&路由數(shù)據(jù)
????????????store.commit("setMenuRouter",?_menu);
????????????//?推入權(quán)限路由列表
????????????router.addRoutes(store.state.sidebar.userRouter);
????????????next({...to,?replace:?true?});
????????????//?}
??????????})
??????????.catch(err?=>?{
????????????//?console.log(err);
????????????//?Message.error("服務(wù)器連接失敗");
??????????});
??????}?else?{
????????//當(dāng)有用戶權(quán)限的時候谣妻,說明所有可訪問路由已生成?如訪問沒權(quán)限的菜單會自動進入404頁面
????????if?(to.path?==?"/login")?{
??????????next({
????????????name:?"index"
??????????});
????????}?else?{
??????????next();
????????}
??????}
????}?else?{
??????//?無登錄狀態(tài)時重定向至登錄?或可進入無需登錄狀態(tài)路徑
??????if?(to.path?==?"/login"?||?to.meta.auth?===?0)?{
????????next();
??????}?else?{
????????next({
??????????path:?"/login"
????????});
??????}
????}
??});
}
```
??>?經(jīng)過需求5改造后的axios的請求攔截器是這樣的萄喳,因為ie無法使用`visibilitychange`,并且嘗試百度其他屬性無效拌禾,因此在請求發(fā)出前做了粗暴處理:
??```
??if?(ie瀏覽器)?{?
??????setLocal('_ie',?Math.random())
??????let?Local_auth?=?getLocal(auth_code,?true);
??????let?Session_auth?=?getSession(auth_code);
??????if?(Local_auth?&&?Local_auth?!=?Session_auth)?{
????????setSession(auth_code,?Local_auth,?true);
????????router.go(0)
????????return?false
??????}
????}
??```
>?這里有一個小問題需要注意:因為用的`local`因此首次打開瀏覽器可能會有登錄已過期的提示取胎,這里相信大家都能找到適合自己的處理方案
??###??結(jié)語
經(jīng)過這些簡單又好用的處理,一個基本滿足需求的前后端分離前端鑒權(quán)方案就誕生啦