<>Vue项目前后端分离下的前端鉴权方案
<>技术栈
前端Vue全家桶,后台.net。
<>需求分析
* 前端路由鉴权,屏蔽地址栏入侵
* 路由数据由后台管理,前端只按固定规则异步加载路由
* 权限控制精确到每一个按钮
* 自动更新token
* 同一个浏览器只能登录一个账号
<>前端方案
对于需求1、2、3,采用异步加载路由方案
* 首先编写vue全局路由守卫
* 排除登录路由和无需鉴权路由
* 登录后请求拉取用户菜单数据
* 在vuex里处理菜单和路由匹配数据
* 将在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因此首次打开浏览器可能会有登录已过期的提示,这里相信大家都能找到适合自己的处理方案
<>结语
经过这些简单又好用的处理,一个基本满足需求的前后端分离前端鉴权方案就诞生啦