抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

使用Vue开发后台管理系统,必然要设计到权限的问题。比如系统的某些页面或者资源(按钮,操作等),需要该用户有对应的权限才能可见、可用。一个完整系统的权限,应当包括这些

  • 接口权限/请求权限
    越权请求要进行拦截
  • 路由权限
    用户登录后只能看到自己权限内的菜单,且只能访问自己权限内的路由地址
  • 视图权限
    包括页面权限,按钮级权限,用户只能看到自己权限的内容和按钮

本文就从以上三个方面回顾下平时我在项目中是如何处理的(尤其是关于路由权限,可以设计的较为复杂),并且将Vue2搭配VueRouter v3和Vue3搭配VueRouter V4的处理方案进行一个比较。

接口权限

用户登录成功后得到token,将token持久化到本地,并通过axios的请求拦截器将token携带在头部

api.interceptors.request.use(config => {
  let token = localStorage.getItem('token')
  const ret = _.assign({}, config, {
    headers: _.assign({}, config.headers, {
      ...token
    }),
  })
  return ret
})

当请求响应返回相应的表示无权限的状态码,这时候重定向到登录页

axios.interceptors.response.use(res => {}, response => {
  if(response.data.code === 6666) {
    router.push('/login')
  }
})

*路由权限

这种方案有很多,此处提供大部分系统常用的几个方案的对比,总结优缺点

注册全部路由

这种比较简单一点,很多系统都是采用的这种方式。在路由初始化的时候挂载全部路由,在路由项上标记响应的权限信息,在全局路由守卫beforeEach中进行校验。总而言之,需要完成这三点

  1. 在路由配置项中做标识,告知该路由的全部权限
  2. 需要一个地方记录该用户所拥有的的全部权限数据,如Vuex + 本地持久化
  3. 在router.beforeEach中结合1,2两点判断

1)路由配置项中做标识

const routes = [
  {path: 'home', component: () => import ('xxx/home.vue'), meta: { roles: ['admin', 'purchase', 'storehouse'] }}
]

2)登录后存储用户权限数据

要保证刷新后用户的信息依然能获取到,会想到localStorage, sesstionStorage, cookie。但是,一般来说,我们不直接将重要的数据保存在这里,容易被人篡改。应该使用vuex存储数据,但是刷新之后会面临数据丢失的问题, 这时候还是需要借助本地持久化存储,不同的是, 我们不直接存储完整数据,而是存储用户id之类的,这样,在刷新导致vuex数据丢失的时候,就能发起请求重新再获取数据。

//types.js

export const SET_RIGHTS = 'SET_RIGHTS'

// permission.js

import * as types from './types'

const state = {
  rights: null
}

const getters = {
  rights: state => state.rights
}

const mutations = {
  [types.SET_RIGHTS](state, value) {
    state.rights = value
  }
}

const actions = {
  setRights({ commit }, value) {
    commit(types.SET_RIGHTS, value)
  }
}

export default {
  state,
  getter,
  mutations,
  actions
}

这里rights默认值要给null而不是[],用来区分是初始化状态还是没有任何权限,在刷新场景下是有使用场景的,通过判断是否为空数组才能判断是刷新了页面

为null又有两种情况

  • 新的tab页创建的页面,或者从别的网站进入
  • 刷新页面

为了区分这两种行为,需要判断web storage里有没有存储用户的userId字段,如果有,代表已经登陆过了,就算没有权限也会是[]

3)导航守卫中校验权限

// router.js

import store from './store'
import getUserInfo from '...'

function hasPermission(roles, route) {
  if(route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.indexOf(role) >= 0)
  } else {
    return true
  }
}

/**
 * 检查进入的路由是否需要权限控制
 * @return {Boolean} 返回true代表没有这个权限,需要进行控制
 * @param {Object} to - 即将进入的路由对象
 * @param {Object} from - 来自的路由对象
 * @param {Function} next - 路由跳转的函数
*/

const verifyRouteAuthority = async (to, from, next) => {
  // 仅对有权限控制需求的页面进行控制,路由表里的meta.roles没有设置或设置为null代表无需控制,[]代表什么权限都没有
  const route = to.matched[to.matched.length - 1]
  const permissionState = store.state.permission  // permission是模块名
  const rights = permissionState.
  if(route.meta.roles != null) {
    // 为null的场景,从空tab进入或其他网站过来;刷新页面
    if(permissionState.rights === null) {
      const userId = sesstionStorage.getItem('userId')
      // 如果是刷新了导致权限配置丢失, 需要重新获取权限
      if(userId) {
        const roles = await getUserInfo().roles
        store.dispatch('setRights', roles)
      } else {
        next({ path: '/' })
        return true
      }
    }

    // 如果是需要进行权限控制的页面,判断是否有权限
    if(!hasPermission(route, permissionStater.rights)) {
      next({ path: '/' })
      return true
    }
  }
  return false
}

router.beforeEach((to, from, next) => {
  // 没有匹配的路由
  if(to.matched.length === 0) {
    next({
      path: '/',
      query: to.query
    })
    return
  }
  const res = await verifyRouteAuthority(to, from ,next)
  if(res) return
})

注销清空权限

store.dispatch('setRights', null)

优缺点

  • 优点:不用动态去注册路由,处理逻辑主要在beforeEach中处理
  • 缺点:注册了很多路由,还有菜单和路由也没有解耦,这个在后文会优化

动态注册路由

这里先着重介绍下vue-router v3版本下的动态路由。在v3版本中,仅提供了addRoutes一个api,这个在使用过程中会出现一些问题,没有办法删除,替换路由。当权限发生变更时,需要追加新路由,旧路由不会被删除;且也可能会发生重复追加同名路由的情况

因此使用addRoutes我们需要解决以下三个问题

  • 切换用户或者身份时,权限发生变化,最理想的情况是删除已经注册的路由,追加新理由
  • 刷新页面时,如果用户鉴权还是通过的,那么权限允许访问的页面依然能进行访问
  • 退出系统,清除已经注册的路由

最暴力的办法就是切换用户或身份时,刷新一下页面,此时路由会重新初始化,事实上很多网站都是这么做的,如果登录系统和当前应用不是一个单页应用时,这种就更没问题,登录后本身就能重新初始化

如果你不是属于以上情况,并且不想刷新时,那么你可以

// router.js

import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);

// 创建路由实例的函数
// 这里的staticRoutes表示你系统的静态路由
const createRouter = () => {
  return new Router({
    routes: staticRoutes
  });
};  

const router = createRouter();
/**
* 重置注册的路由导航map
* 主要是为了通过addRoutes方法动态注入新路由时,避免重复注册相同name路由
*/

const resetRouter = () => {
  const newRouter = createRouter();
  router && (router.matcher = newRouter.matcher);
};

export { resetRouter };
export default router;
//store.js
import {
  asyncRouterMap
} from '@/router/index'
import {
  SET_ROUTERS
} from '**types'  // 伪代码

const permission = {
  namespaced: true,
  state: {
    addRoutes: [],
  },
  mutations: {
    [SET_ROUTERS]: (state, routes) => {
      state.addRoutes = routes
    }
  },
  actions: {
    GenerateRoutes({ commit }, data) {            
      return new Promise(resolve => {
        const { roles } = data
        let accessedRouters
        if (roles.indexOf('admin') >= 0) {
          console.log('admin>=0')
          accessedRouters = asyncRouterMap
        } else {
          console.log('admin<0')
          accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
          // accessedRouters = ''
          // accessedRouters = asyncRouterMap
        }
        console.log('accessedRouters', accessedRouters)
        commit('SET_ROUTES', accessedRouters)    
        resolve()
      })
    }
  }
}

function hasPermission(roles, route) {
  if(route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.indexOf(role) >= 0)
  } else {
    return true
  }
}

//根据角色、过滤出路由列表
export function filterAsyncRouter(asyncRouterMap, roles) {
  const accessedRoutes = asyncRouterMap.filter((route) => {
    if (hasPermission(roles, route)) {
      if (route.children && route.children.length) {
        route.children = filterAsyncRouter(route.children, roles);
      }
      return true;
    }
    return false;
  });
  return accessedRoutes;
}

export default permission;

接下来需要在router.beforeEach中动态的去注册路由,为方便,新建permission.js注册逻辑,在入口文件引入即可

// permission.js

import router from './router'
import store from './store'
import cookie from 'js-cookie'

// 不需要登录就可以访问的页面
const whiteList = ["/", "/404", "/401"];
router.beforeEach((to, from, next) => {
  const shiroCookie = cookie.get("userInfo");
  if (shiroCookie) {
    if (store.state.roles.length === 0) {
      // 登录操作后,以及当刷新页面是store中的数据恢复到初始值,需要重新设置
      const roles = [JSON.parse(cookie.get("userInfo")).position];
      store.commit("SET_ROLES", roles);
      store.dispatch("GenerateRoutes", { roles }).then(() => {
        // d根据roles权限生成可访问的路由表
        router.addRoutes(store.state.addRouters); // 动态添加可访问路由表
        next({ ...to, replace: true }); // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
      });
    } else {
      // 没有刷新页面对路由权限验证
      if (to.meta.roles && to.meta.roles.length) {
        // 当前路由有权限限制时,经过验证后,允许跳转
        if (hasPermission(store.state.roles, to.meta.roles)) {
          next();
        }
      } else {
        // 不存在权限限制时,则允许跳转
        next();
      }
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) {
      // 如果在白名单之列,则允许跳转
      next();
    } else {
      // 如果不在白名单之列,则返回登录页
      next("/");
    }
  }
});

登录时往cookie中存储userInfo;注销时,清空cookie的userInfo,并清空store中roles,以及resetRouter

在每次通过addRoutes追加路有前先resetRouter重置一下路由映射,再追加。但是其实addRoutes还存在一个问题,如果静态路由中有子路由,再追加该路由的子路由时,会重复添加,这个时候只能人为约定不能为静态路由追加子孙路由了

用户交流区