###?技術棧
?前端Vue全家桶,後臺.net。
###?需求分析
?1.?前端路由鑒權,屏蔽地址欄入侵
?2.?路由數據由後臺管理,前端只按固定規則異步加載路由
?3.?權限控制精確到每壹個按鈕
?4.?自動更新token
?5.?同壹個瀏覽器只能登錄壹個賬號
###?前端方案
>?對於需求1、2、3,采用異步加載路由方案
?1.?首先編寫vue全局路由守衛
?2.?排除登錄路由和無需鑒權路由
?3.?登錄後請求拉取用戶菜單數據
?4.?在vuex裏處理菜單和路由匹配數據
?5.?將在vuex裏處理好的路由數據通過`addRoutes`異步推入路由
?```
router.beforeEach((to,?from,?next)?=>?{
?//?判斷當前用戶是否已拉取權限菜單
?if?(store.state.sidebar.userRouter.length?===?0)?{
//?無菜單時拉取
getMenuRouter()
?.then(res?=>?{
let?_menu?=?res.data.Data.ColumnDataList?||?[];
//?if?(res.data.Data.ColumnDataList.length?>?0)?{
//?整理菜單&路由數據
store.commit("setMenuRouter",?_menu);
//?推入權限路由列表
router.addRoutes(store.state.sidebar.userRouter);
next({...to,?replace:?true?});
//?}
?})
?.catch(err?=>?{
//?console.log(err);
//?Message.error("服務器連接失敗");
?});
?}?else?{
//當有用戶權限的時候,說明所有可訪問路由已生成?如訪問沒權限的菜單會自動進入404頁面
if?(to.path?==?"/login")?{
?next({
name:?"index"
?});
}?else?{
?next();
}
?}
}?else?{
?//?無登錄狀態時重定向至登錄?或可進入無需登錄狀態路徑
?if?(to.path?==?"/login"?||?to.meta.auth?===?0)?{
next();
?}?else?{
next({
?path:?"/login"
});
?}
}
?});
?```
?#####?註意
?>?我這裏無需鑒權的路由直接寫在router文件夾下的index.js,通過路由元信息meta攜帶指定標識
?```
{
?path:?"/err-404",
?name:?"err404",
?meta:?{
?authentication:?false
?},
?component:?resolve?=>?require(["../views/error/404.vue"],?resolve)
},
?```
?>?上面說到路由是根據後臺返回菜單數據根據壹定規則生成,因此壹些不是菜單,又需要登錄狀態的路由,我寫在router文件夾下的router.js裏,在上面步驟4裏處理後臺返回菜單數據時,和處理好的菜單路由數據合並壹同通過`addRoutes`推入。?
?這樣做會有壹定的被地址欄入侵的風險,但是筆者這裏大多是不太重要的路由,如果妳要求咳咳,可以定壹份字典來和後臺接口配合精確加載每壹個路由。
?```
?//?加入企業
?{
path:?"/join-company",
name:?"join-company",
component:?resolve?=>?require([`@/views/index/join-company.vue`],?resolve)?
?},
?```
?>?在vuex中將分配的菜單數據轉化為前端可用的路由數據,我是這樣做的:
?管理系統在新增菜單時需要填寫壹個頁面地址字段`Url`,前端得到後臺菜單數據後根據`Url`字段來匹配路由加載的文件路徑,每個菜單壹個文件夾的好處是:妳可以在這裏拆分js、css和此菜單私有組件等
?```
?menu.forEach(item?=>?{
let?routerItem?=?{
?path:?item.Url,
?name:?item.Id,
?meta:?{
auth:?item.Children,
?},?//?路由元信息?定義路由時即可攜帶的參數,可用來管理每個路由的按鈕操作權限
?component:?resolve?=>
require([`@/views${item.Url}/index.vue`],?resolve)?//?路由映射真實視圖路徑
};
routerBox.push(routerItem);
});
?```
?>?關於如何精確控制每壹個按鈕我是這樣做的,將按鈕編碼放在路由元信息裏,在當前路由下匹配來控制頁面上的按鈕是否創建。
?菜單數據返回的都是多級結構,每個菜單下的子集就是當前菜單下的按鈕權限碼數組,我把每個菜單下的按鈕放在此菜單的路由元信息`meta.auth`中。這樣作的好處是:按鈕權限校驗只需匹配每個菜單路由元信息下的數據,這樣校驗池長度通常不會超過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的響應攔截器中
if?(res.headers.auth)?{
?setSession(auth_code,?res.headers.auth);
}
?```
?>?對於需求5的處理比較麻煩,要跨tab頁只能通過`cookie`或`local`,筆者這裏不允許使用`cookie`因此采用的`localstorage`。通過打開的新頁面讀取`localstorage`內的`token`數據來同步多個頁面的賬號信息。`token`使用的`jwt`並前端md5加密。
?這裏需要註意壹點是頁面切換要立即同步賬號信息。
?>?經過需求5改造後的全局路由守衛是這樣的:
?```
function?_AUTH_()?{
?//?切換窗口時校驗賬號是否發生變化
?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)?=>?{
?//?判斷當前用戶是否已拉取權限菜單
?if?(store.state.sidebar.userRouter.length?===?0)?{
//?無菜單時拉取
getMenuRouter()
?.then(res?=>?{
let?_menu?=?res.data.Data.ColumnDataList?||?[];
//?if?(res.data.Data.ColumnDataList.length?>?0)?{
//?整理菜單&路由數據
store.commit("setMenuRouter",?_menu);
//?推入權限路由列表
router.addRoutes(store.state.sidebar.userRouter);
next({...to,?replace:?true?});
//?}
?})
?.catch(err?=>?{
//?console.log(err);
//?Message.error("服務器連接失敗");
?});
?}?else?{
//當有用戶權限的時候,說明所有可訪問路由已生成?如訪問沒權限的菜單會自動進入404頁面
if?(to.path?==?"/login")?{
?next({
name:?"index"
?});
}?else?{
?next();
}
?}
}?else?{
?//?無登錄狀態時重定向至登錄?或可進入無需登錄狀態路徑
?if?(to.path?==?"/login"?||?to.meta.auth?===?0)?{
next();
?}?else?{
next({
?path:?"/login"
});
?}
}
?});
}
```
?>?經過需求5改造後的axios的請求攔截器是這樣的,因為ie無法使用`visibilitychange`,並且嘗試百度其他屬性無效,因此在請求發出前做了粗暴處理:
?```
?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`因此首次打開瀏覽器可能會有登錄已過期的提示,這裏相信大家都能找到適合自己的處理方案
?###?結語
經過這些簡單又好用的處理,壹個基本滿足需求的前後端分離前端鑒權方案就誕生啦