Vue-Access-Control:前端用户权限控制解决方案

原文地址:http://refined-x.com/2017/11/28/Vue2.0用户权限控制解决方案/

 

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

总体思路

会话开始之初,先初始化一个只有登陆路由的Vue实例,在根组件created钩子里将路由定向到登陆页,用户登陆成功后前端拿到用户token,设置axios实例统一为请求headers添加{"Authorization":token}实现用户鉴权,而后获取当前用户的权限数据,主要包括路由权限和资源权限,以后动态添加路由,生成菜单,实现权限指令和全局权限验证方法,并为axios实例添加请求拦截器,至此完成权限控制初始化。动态加载路由后,路由组件将随之加载并渲染,然后展示前端界面。vue

为解决浏览器刷新路由重置的问题,拿到token后要将其保存到sessionStorage,根组件的created钩子负责检查本地是否已有token,若是有则无需登陆直接用该token获取权限并初始化,若是token有效且当前路由有权访问,将加载路由组件并正确展示;若当前路由无权访问将按路由设置跳转404;若是token失效,后端应返回4xx状态码,前端统一为axios实例添加错误拦截器,遇到4xx状态码执行退出操做,清除sessionStorage数据并跳转到登陆页,让用户从新登陆。webpack

最小依赖原则

Vue-Access-Control的定位是单一领域解决方案,除了Vue/Vue-Router/axios以外没有其余依赖,理论上能够无障碍的应用到任何有权限控制需求的Vue项目中,项目基于webpack 模板开发构建,大多数新项目能够直接基于检出代码继续开发。须要说明的是,项目额外引入的Element-UICryptoJS仅用于开发演示界面,他们不是必须且与权限控制毫无关系,项目应用中能够自行取舍。ios

目录结构

src/
  |-- api/                  //接口文件
  |     |-- index.js             //输出通用axios实例
  |     |-- account.js           //按业务模块组织的接口文件,全部接口都引用./index提供的axios实例
  |-- assets/
  |-- components/
  |-- router/
  |     |-- fullpath.js         //完整路由数据,用于匹配用户的路由权限获得实际路由
  |     `-- index.js            //输出基础路由实例
  |-- views/
  |-- App.vue
  ·-- main.js

 

数据格式约定

  • 路由权限数据必须是以下格式的对象数组,idparent_id相同的两个路由具备上下级关系,若是但愿使用自定义格式的路由数据,须要修改路由控制的相关实现,详见路由控制数据格式约定git

  • [
        {
          "id": "1",
          "name": "菜单1",
          "parent_id": null,
          "route": "route1"
        },
        {
          "id": "2",
          "name": "菜单1-1",
          "parent_id": "1",
          "route": "route2"
        }
      ]  
  • 资源权限数据必须是以下格式的对象数组,每一个对象表明一个RESTful请求,支持带参数的url,具体格式说明见请求控制github

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

     

路由控制

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

动态注册路由

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

[{
  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'
}]

  


那么接下来就须要获取首页以及其子路由们,思路是事先在本地存一份整个项目的完整路由数据,而后根据用户权限对完整路由进行筛选。编程

筛选的实现思路是先将后端返回的路由数据处理成以下哈希结构:axios

let hashMenus = {
   "/route1":true,
   "/route1/route1-1":true,
   "/route1/route1-2":true,
   "/route2":true,
   ...
}

  

而后遍历本地完整路由,在循环中将路径拼接成上述结构中的key格式,经过hashMenus[route]就能够判断路由是否匹配,具体实现见App.vue文件中的getRoutes()方法。

若是后端返回的路由权限数据与约定不一样,就须要自行实现筛选逻辑,只要能获得实际可用的路由数据就能够,最终使用addRoutes()方法将他们动态添加到路由实例中,注意404页面的模糊匹配必定要放在最后。

动态菜单

路由数据能够直接用来生成导航菜单,但路由数据是在根组件中获得的,导航菜单存在于index.vue组件中,显然咱们须要经过某种方式共享菜单数据,方法有不少,通常来讲首先想到的是Vuex,但菜单数据在整个用户会话过程当中不会发生改变,这并非Vuex的最佳使用场景,并且为了尽可能减小没必要要的依赖,这里用了最简单直接的方法,把菜单数据挂在根组件data.menuData上,在首页里用this.$parent.menuData获取。

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

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

菜单的具体实现能够参考views/index.vue

视图控制

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

全局验证方法

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

请求对象格式:

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

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

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

  

权限验证方法的具体实现见App.vueVue.prototype.$_has方法。

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

具体实现细节参考基于Vue实现后台系统权限控制中的相关章节。

自定义指令

v-if的响应特性是把双刃剑,由于判断表达式在运行过程当中会频繁触发,但实际上在一个用户会话周期内其权限并不会发生变化,所以若是只须要校验权限的话,用v-if会产生大量没必要要的运算,这种状况只需在视图载入时校验一次便可,能够经过自定义指令实现:

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

  

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

请求控制

请求控制是利用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不须要转化格式。

拦截器的具体实现见App.vue中的setInterceptor()方法。

若是你的项目还须要其余的通配符格式,只须要在拦截器中实现对应的检测和转化方法就能够了。

演示及说明

演示说明:

DEMO项目中演示了动态菜单、动态路由、按钮权限、请求拦截。

演示项目后端由rap2生成mock数据,登陆请求一般应该是POST方式,但由于rap2的编程模式没法获取到非GET的请求参数,所以只能用GET方式登陆,实际项目中不建议仿效;

另外登陆后获取权限的接口原本不须要携带额外参数,后端能够根据请求头携带的token信息实现用户鉴权,但由于rap2的编程模式获取不到headers数据,所以只能增长一个”Authorization”参数用于生成模拟数据。

测试帐号:

1. username: root
   password: 任意
2. username: client
   password: 任意

  

演示地址:

vue-access-control.refined-x.com

相关文章
相关标签/搜索