基于vue2 + koa 2.0的前后端登录权限和路由权限控制实践

9,319 阅读5分钟

前后台交互过程中,涉及到用户登陆权限和前端路由模块是比较复杂的模块,这里需要我们理清楚整个过程,才能把整个工程架构搭起来。其实在我之前的一篇文章(juejin.cn/post/698328… 中也大体讨论过这个过程。现在我通过具体的前后端项目实例来进行梳理,目的是为了一次性弄清整个过程。本文主要是基于实例项目来讲解整个登录权限前端路由控制权限是如何做的。

1. 实现前后端登录认证

本文中的前后端登陆验证是通过 token 认证机制来实现的,基于Koa 2.0的后台框架在登陆成功之后,就会生成token,客户端拿到并保存生成的token,在下一次请求是会在请求头自动携带token,在服务端进行验证(本文是通过jwt签名验证来实现前后端身份信息的验证的)。

users.post('/login', async (ctx, next) => {
  const { name, password } = ctx.request.body;
  const validate = Validate.loginCheck({ name, password });
  if (validate) {
    ctx.throw(400, '参数错误!');
  }
  const sqlData = await model.findUserData(name, password);  // 查找用户是否存在
  if (sqlData && sqlData.length > 0) {
    // 登录后获取token值
    const userId = sqlData[0].userId;
    const token = jwtService.sign({
      userId
    });
    ctx.body = { code: 200, data: token };
  } else {
    ctx.throw(400, '登录账户或者密码错误!');
  }
})

上面的代码中,登陆成功时生成 jwt签名的token(通过jwtService.js 文件的封装的sign方法实现),那么当前端携带token 回来怎么做签名验证呢?一般的方法是通过后台本地缓存token,然后和前端携带过来的token 进行对比验证(通过jwtService.js 文件的封装的verify方法实现),本文中我们不不需要后台本地缓存token,通过签名验证来实现:

/**
 * 通过前端的取到的token 来验证并返回当前用户信息
 */
users.post('/checkToken', async (ctx, next) => {
  const { token } = ctx.request.body;
  if (!token) {
    ctx.throw(401, "请先登录账号");
  }
  let data = jwtService.verify(token);   // 通过取到的token 信息验证当前用户
  if (!data || !data.userId) {
    ctx.throw(401, "token已经过期,请重新登录");
  }
  const sqlData = await model.findUserById(data.userId);  // 查找用户是否存在
  const userList = {
    roles: [sqlData[0].roles],
    name: sqlData[0].name,
    avatar: sqlData[0].avatar
  }
  ctx.body = {
    code: 200,
    data: {
      userList
    }
  }
})
// 代码文件 jwtService.js
const jwt = require("jsonwebtoken");
const processEnv = process.env; 
const JWT_SECRET = processEnv.JWT_SECRET || "n9r5tiv5";
module.exports = {
    /**
     * 获取jwt token
     * @param data
     * @returns {*}
     */
    sign(data) {
      let config = 24 * 3600;
      let token = jwt.sign(
        {
          exp:
            Math.floor(Date.now() / 1000) + config ,
          data: data
        },
        JWT_SECRET
      );
      return token;
    },
    /**
     * 验证token,返回不是null就是通过验证
     * @param token
     * @returns {data | null}
     */
    verify(token) {
      try {
        let decode = jwt.verify(token, JWT_SECRET);
        return decode.data;
      } catch (err) {
        // 不处理err
        return null;
      }
    }
  };

2. 前端路由权限控制

我们来简单分析一下本系统路由权限逻辑:

  1. 我们通过是否有权限将路由对象区分非权限路由对象权限路由对象。初始化时,将非权限路由对象赋值给Router,同时设置权限路由中的meta对象,如:meta:{roles:['admin','editor']}表示该roles所拥有的路由权限;
  2. 通过用户登录成功之后返回的roles值,进行路由的匹配并生成新的路由对象;
  3. 用户成功登录并跳转到首页时,根据刚刚生成的路由对象,渲染左侧的菜单,不同的用户看到的菜单是不一样的。

基于上面前后端的登陆验证的实现,我们结合路由控制权限来分析用户在登录页面和退出页面时候的逻辑,该过程主要在路由守卫阶段来完成,即通过路由导航钩子router.beforeEach()函数确定下一步的跳转逻辑:

  • 如果用户没有登录,系统初始化页面会直接跳转到登录页面,输入用户、密码登录;

  • 如果用户已经登录成功,并且已经拿到token值,此时如果用户访问登录页面,直接定位到登录页面; 如果用户访问非登录页面,需要根据用户是否缓存有roles信息来进行不同业务逻辑: (1)、初始情况下,用户roles信息为空:

    1. 通过sysGetUserInfo()函数,根据token拉取用户信息;并通过store将该用户roles,name等信息存储于vuex中;
    2. 通过store.dispatch('GenerateRoutes', { roles })去重新过滤和生成当前用户角色对应的权限路由,通过router.addRoutes()合并路由表;
    3. 如果在获取用户信息接口时出现错误,则调取store.dispatch('LogOut')接口,返回到login页面;
      (2)、用户已经拥有roles信息:

    1.点击页面路由,通过roles权限判断 hasPermission()。如果用户有该路由权限,直接跳转对应的页面;如果没有权限,则跳转至401提示页面;

核心逻辑的实现代码如下:

/**
 * 判断当前是否有路由跳转的权限
 * @param {*} roles 
 * @param {*} permissionRoles 
 * @returns 
 */
function hasPermission(roles, permissionRoles) {
  if (roles.indexOf('admin') >= 0) return true 
  if (!permissionRoles) return true
  return roles.some(role => permissionRoles.indexOf(role) >= 0)
}

router.beforeEach((to, from, next) => {
  console.log('路由守卫', to);
  NProgress.start()  // 显示页面顶部进度条
   // 设置浏览器头部标题
   const browserHeaderTitle = to.meta.title
   store.commit('SET_BROWSERHEADERTITLE', {
     browserHeaderTitle: browserHeaderTitle
   })
  // 点击登录时,拿到了token并存入了cookie,保证页面刷新时,始终可以拿到token
  if (getToken('Token')) {
    if(to.path === '/login') {
      next({ path: '/' })  
      NProgress.done() 
    } else {
      // 用户登录成功之后,每次点击路由都进行了角色的判断;
      if (store.getters.roles.length === 0) {
        let token = getToken('Token');
        sysGetUserInfo({"token": token}).then().then(res => { // 根据token拉取用户信息
          let userList = res.data.userList;
          store.commit("SET_ROLES",userList.roles);
          store.commit("SET_NAME",userList.name);
          store.commit("SET_AVATAR",userList.avatar);
          // 通过取到当前用户的权限信息,来生成权限路由表:
          store.dispatch('GenerateRoutes', { "roles":userList.roles }).then(() => { // 根据roles权限生成可访问的路由表
            router.addRoutes(store.getters.addRouters) // 动态添加可访问权限路由表
            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
          })
        }).catch((err) => {
          store.dispatch('LogOut').then(() => {
            Message.error(err || 'Verification failed, please login again')
            next({ path: '/' })
          })
        })
      } else {
        // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
        if (hasPermission(store.getters.roles, to.meta.roles)) {
          next()//
        } else {
          next({ path: '/401', replace: true, query: { noGoBack: true }})
        }
      }
    }
  } else {
    // 没有取到token,且路由是 /login
    if (['/login'].indexOf(to.path) !== -1) {
      next()
    } else {
      next('/login')
      NProgress.done()
    }
  }
})

本文前后端源码: