Vue-Access-Control

1.介绍

       Vue-Access-Control是一套基于Vue/Vue-Router/axios 实现的前端用户权限控制解决方案,通过对路由、视图、请求三个层面的控制,使开发者可以实现任意颗粒度的用户权限控制。

2.项目地址

仓库地址:https://github.com/tower1229/Vue-Access-Control

项目主页:http://refined-x.com/Vue-Access-Control/

演示地址:vue-access-control.refined-x.com

测试账号:

username:root
password:任意

username:client
password:任意

3.权限控制流程图

4.数据约定

 路由数据格式:路由权限数据必须是如下格式的对象数组,idparent_id相同的两个路由具有上下级关系

[
    {
      "id": "1",
      "name": "菜单1",
      "parent_id": null,
      "route": "route1"
    },
    {
      "id": "2",
      "name": "菜单1-1",
      "parent_id": "1",
      "route": "route2"
    }
  ]

资源数据格式:

 [
    {
      "id": "2c9180895e172348015e1740805d000d",
      "name": "账号-获取",
      "url": "/accounts",
      "method": "GET"
    },
    {
      "id": "2c9180895e172348015e1740c30f000e",
      "name": "账号-删除",
      "url": "/account/**",
      "method": "DELETE"
    }
]

5.路由控制

路由控制包括动态注册路由和动态生成菜单两部分。

(1)动态注册路由

最初实例化的路由仅包括登录和404两个路径,我们期待完整的路由是这样的:

[{
  path: '/login',
  name: 'login',
  component: (resolve) => require(['../views/login.vue'], resolve)
}, {
  path: '/404',
  name: '404',
  component: (resolve) => require(['../views/common/404.vue'], resolve)
}, {
  path: '/',
  name: '首页',
  component: (resolve) => require(['../views/index.vue'], resolve),
  children: [{
    path: '/route1',
    name: '栏目1',
    meta: {
      icon: 'icon-channel1'
    },
    component: (resolve) => require(['../views/view1.vue'], resolve)
  }, {
    path: '/route2',
    name: '栏目2',
    meta: {
      icon: 'ico-channel2'
    },
    component: (resolve) => require(['../views/view2.vue'], resolve),
    children: [{
      path: 'child2-1',
      name: '子栏目2-1',
      meta: {
        
      },
      component: (resolve) => require(['../views/route2-1.vue'], resolve)
    }]
  }]
}, {
  path: '*',
  redirect: '/404'
}]

那么接下来就需要获取首页以及其子路由们,思路是事先在本地存一份整个项目的完整路由数据,然后根据用户权限对完整路由进行筛选。最终使用addRoutes()方法将他们动态添加到路由实例中,注意404页面的模糊匹配一定要放在最后。

(2)动态菜单

路由数据可以直接用来生成导航菜单,但路由数据是在根组件中得到的,导航菜单存在于其它组件中,显然我们需要通过某种方式共享菜单数据,方法有很多,一般来说首先想到的是Vuex,但菜单数据在整个用户会话过程中不会发生改变,这并不是Vuex的最佳使用场景,而且为了尽量减少不必要的依赖,这里用了最简单直接的方法,把菜单数据挂在根组件上this.$root.menuData,在别的组件里用this.$root.menuData获取。

另外,导航菜单很可能会有添加栏目图标的需求,这可以通过在路由中添加meta数据实现,例如将图标class或unicode存到路由meta里,模板中就可以访问到meta数据,用来生成图标标签。

【注意】在多角色系统中可能遇到的一个问题是,不同角色有一个名字相同但功能不同的路由,比如说系统管理员企业管理员都有”账号管理”这个路由,但他们的操作权限和目标不同,实际上是两个完全不同的界面,而Vue不允许多个路由同名,因此路由的name必须做区分,但把区分后的name显示在前端菜单上会很不美观,为了让不同角色可以享有同一个菜单名称,我们只要将这两个路由的meta.name都设置成”账号管理”,在模板循环时优先使用meta.name就可以了。

6.视图控制

视图控制的目标是根据当前用户权限决定界面元素显示与否,典型场景是对各种操作按钮的显示控制。实现视图控制的本质是实现一个权限验证方法,输入请求权限,输出是否获准。然后配合v-ifjsx或自定义指令就能灵活实现各种视图控制。

(1)全局验证方法

验证方法的的实现本身很简单,无非是根据后端给出的资源权限做判断,重点在于优化方法的输入输出,提升易用性,经过实践总结最终使用的方案是,将权限跟请求同时维护,验证方法接收请求对象数组为参数,返回是否具有权限的布尔值。

请求对象的格式:

//获取账户列表
const request = {
  p: ['get,/accounts'],
  r: params => {
    return instance.get(`/accounts`, {params})
  }
}

权限验证方法$_hash()的调用格式:

v-if="$_has([request])"

权限验证方法的具体实现:

Vue.prototype.$_has = function(rArray) {

          let RequiredPermissions = [];
          let permission = true;

          if (Array.isArray(rArray)) {

            rArray.forEach(e => {
              if(e && e.p){
                RequiredPermissions = RequiredPermissions.concat(e.p);
              }
            });
          } else {

            if(rArray && rArray.p){
              RequiredPermissions = rArray.p;
            }
            
          }
          /*
          * 遍历所需权限的数组并与实际权限进行对比!
          * */
          for(let i=0;i<RequiredPermissions.length;i++){
            let p = RequiredPermissions[i];

            if (!resourcePermission[p]) {
              permission = false;
              break;
            }
          }
          
          return permission;
        }

将权限验证方法全局混入,就可以在项目中很容易的配合v-if实现元素显示控制,这种方式的优点在于灵活,除了可以校验权限外,还可以在判断表达式中加入运行时状态做更多样性的判断,而且可以充分利用v-if响应数据变化的特点,实现动态视图控制。

(2)自定义指令

v-if的响应特性是把双刃剑,因为判断表达式在运行过程中会频繁触发,但实际上在一个用户会话周期内其权限并不会发生变化,因此如果只需要校验权限的话,用v-if会产生大量不必要的运算,这种情况只需在视图载入时校验一次即可,可以通过自定义指令实现:

//全局的权限指令
Vue.directive('has', {
  bind: function(el, binding) {
    if (!Vue.prototype.$_has(binding.value)) {
      el.parentNode.removeChild(el);
    }
  }
});

自定义指令内部仍然是调用全局验证方法,但优点在于只会在元素初始化时执行一次,多数情况下都应该使用自定义指令实现视图控制。

7.请求控制

请求控制是利用axios拦截器实现的,目的是将越权请求在前端拦截掉,原理是在请求拦截器中判断本次请求是否符合用户权限,以决定是否拦截。

普通请求的判断很容易,遍历后端返回的的资源权限格式,直接判断request.methodrequest.url是否吻合就可以了,对于带参数的url需要使用通配符,这里需要根据项目需求前后端协商一致,约定好通配符格式后,拦截器中要先将带参数的url处理成约定格式,再判断权限,方案中已经实现了以下两种通配符格式:

1. 格式:/resources/:id
   示例:/resources/1
   url: /resources/**
   解释:一个名词后跟一个参数,参数通常表示名词的id
   
2. 格式:/store/:id/member
   示例:/store/1/member
   url:/store/*/member
   解释:两个名词之间夹带一个参数,参数通常表示第一个名词的id

对于第一种格式需要注意的是,如果你要发起一个url为"/aaa/bbb"的请求,默认会被处理成"/aaa/**"进行权限校验,如果这里的”bbb”并不是参数而是url的一部分,那么你需要将url改成"/aaa/bbb/",在最后加一个”/“表示该url不需要转化格式。

拦截器的实现:

/*
    * 添加一个请求拦截器
    * 1. 获得请求路径
    * 2. 处理请求url(处理的请求主要就是带有参数的请求)
    * 3. 处理后的url与资源对象进行匹配,若是有的话就出发对应的r,也就是发起请求,反之给出越权提示!
    * */
    setInterceptor: function(resourcePermission){

      console.log('触发请求拦截器')

      myInterceptor = instance.interceptors.request.use(config => {

        // 获得去掉参数的请求路径
        let perName = config.url.replace(config.baseURL, '').split('?')[0];

        //RESTful type 1 /path/**
        let reg1 = perName.match(/^(\/[^\/]+\/)[^\/]+$/);
          if (reg1) {
              perName = reg1[1] + '**';
          }

          //RESTful type 2 /path/*/path
          let reg2 = perName.match(/^\/[^\/]+\/([^\/]+)\/[^\/]+$/);
          if (reg2) {
              perName = perName.replace(reg2[1], '*');
          }

        // 检查资源对象里面有没有改请求对应的资源,若是没有,显示提示;若是有就放过,什么都不做!
        
        if (!resourcePermission[config.method + ',' + perName]) {
          this.$message({
            message: '无访问权限,请联系企业管理员',
            type: 'warning'
          });
          return Promise.reject({
            message: 'no permission'
          });
        }

        return config;
      });

    },

8.说明

演示项目后端由rap2生成mock数据,登录请求通常应该是POST方式,但因为rap2的编程模式无法获取到非GET的请求参数,因此只能用GET方式登录,实际项目中不建议仿效;

另外登录后获取权限的接口本来不需要携带额外参数,后端可以根据请求头携带的token信息实现用户鉴权,但因为rap2的编程模式获取不到headers数据,因此只能增加一个”Authorization”参数用于生成模拟数据。

拓展:Java后台获取token的方法

1.通过cookies

2.通过请求头

3.通过请求参数


String token =null;


Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length != 0) {
    Cookie[] var = cookies;
    int var2 = cookies.length;
    for(int var3 = 0; var3 < var; ++var3) {
        Cookie cookie = var[var3];
        if (cookie.getName().equals("token")) {
       token =cookie.getValue();
}
    }
if (StringUtils.isEmpty(token)) {
    token = request.getHeader("token");
    if (StringUtils.isEmpty(token)) {
        token = request.getParameter("token");
}
}

猜你喜欢

转载自blog.csdn.net/FullStackDeveloper0/article/details/85215194