阅读 1240

如果设计一个完善的前端鉴权方案

Vue前端鉴权方案,前后端分离

技术栈

前端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页只能通过cookielocal,笔者这里不允许使用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因此首次打开浏览器可能会有登录已过期的提示,这里相信大家都能找到适合自己的处理方案

结语

经过这些简单又好用的处理,一个基本满足需求的前后端分离前端鉴权方案就诞生啦

文章分类
前端
文章标签