基于Vue实现后台系统权限控制的示例代码


Posted in Javascript onAugust 29, 2017

用Vue/React这类双向绑定框架做后台系统再适合不过,后台系统相比普通前端项目除了数据交互更频繁以外,还有一个特别的需求就是对用户的权限控制,那么如何在一个Vue应用中实现权限控制呢?下面是我的一点经验。

权限控制是什么

在权限的世界里服务端提供的一切都是资源,资源可以由请求方法+请求地址来描述,权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源。具体的说,前端对资源的访问通常是由界面上的按钮发起,比如删除某条数据;或由用户进入某一个页面发起,比如获取某个列表数据。这两种形式覆盖了资源请求的大部分场景,因此权限控制也可以被笼统的分成菜单权限控制和按钮权限控制。

Vue菜单权限控制

菜单是对路由的直接体现,菜单控制实际上就是路由控制。实现路由控制一个简单的方式是,在路由的before钩子里校验当前即将跳转的路由地址是否有权访问,根据校验结果决定路由是否放行,伪码:

router.beforeEach((to, from, next) => {
  //权限校验
  let pass = valid(to);
  if(!pass){
    return console.log('无权访问');
  }
  next();
});

这种实现方式既简单又直观,用于简单的系统非常合适,但这么做本质上是将所有路由全部注册了,直接带来的缺点有两个:一、如果路由组件不是按需加载的话,应用将加载大量冗余代码;二、每次跳转都要遍历一次完整路由是对计算能力的浪费,对于路由总数较大的应用很不可取。

理想的实现方式是本地保存完整路由,但并不立即初始化Vue应用,待用户登录拿到权限后,用菜单权限筛选出可用路由,再用可用路由初始化Vue应用。也就是说,要将登录页独立出去做成一个单独的页面,登录后将用户数据保存在本地,再通过url跳转到Vue应用所在页面,Vue应用启动前通过本地用户数据完成路由筛选,然后初始化Vue应用,伪码如下:

//main.js
let user = sessionStorage.getItem('user');
if (user) {
  user = JSON.parse(user);
  //筛选得到实际路由
  let fullPath = require('fullPath.js');
  let routes = filter(fullPath, user.menus);
  //创建路由对象
  let router = new Router({routes});
  //生成Vue实例
  new Vue({
    el: '#app',
    router,
    render: h => h(App)
  });
}else{
  location.href = '/login/';
}

这样我们就根据用户权限生成了一套”定制”路由,这时我们还希望能直接用路由生成导航菜单,常规的路由数据可能无法满足菜单组件的需求,所以我们可以事先在路由的meta数据里维护上菜单数据,比如菜单名称菜单图标等,只要在模板中通过$router.options就可以访问到当前路由数据,如果使用element-ui的菜单组件实现,代码大致是这样的:

<el-menu router>
  <el-menu-item v-for="(route, index) in $router.options.routes[2].children"
  :route="route"
  :index="route.name">
    <i class="ion" v-html="route.icon"></i>{{route.name}}
  </el-menu-item>
</el-menu>

当然这样只能循环出一级菜单,如果还有二级路由需要对应二级菜单的话,就得判断并循环children节点,比较简单就不放更多代码了,菜单权限控制到这里就完成了。

Vue按钮权限控制

按钮权限控制与菜单权限控制的实现思路类似,也是根据用户权限判断各个按钮的显示与否,方式无非是v-if或自定义指令,而且只要将v-if背后的权限校验逻辑抽象成方法,无论是代码量还是使用形式上都跟自定义指令几乎一样,但v-if的特点是它会响应数据变化,因此随着应用的运行会频繁触发权限校验,而权限在应用的整个生命周期内其实只需校验一次,为了避免无谓的程序执行,这里可以用自定义指令来实现,伪码:

Vue.directive('has', {
 bind: function (el, binding) {
  if(!has(binding.value)){
    el.parentNode.removeChild(el);
  }
 }
});

//用法:
<btn v-has='get,/sources'>按钮</btn>

注意在指令bind回调里有一个has()方法,这就是权限校验方法,我们同时将这个方法全局混合到Vue对象中,使应用里的每个组件都可以访问到这个方法,便于为界面上的v-if提供支持,例如:

<div v-if="has('get,/sources') && something">
  一个需要同时具备'get,/sources'权限和somthing为真值才显示的div
</div>

这样一来凡是需要依据权限实现的按钮显隐控制和界面变化都可以很方便的实现。

但按钮权限控制真正麻烦的地方不在于如何实现,而在于高昂的维护成本。我们假设按钮Btn绑定了点击回调Fn,回调Fn里发起了请求Req,请求Req需要某个资源的访问权限,最终你要根据用户是否拥有Req的权限,决定Btn是否显示,而Req跟Btn之间并没有直接关联,所以我们就要人肉维护他们的关系,一个复杂项目里的按钮有个几十上百都很正常,随着业务的变更去维护这么多按钮的权限,想想都头疼。

有一个方法可以绕开这个烂摊子,那就是前端放弃对视图层的控制,退到请求层面,在请求发起前集中拦截,这时可以直接根据请求方法和请求地址来校验权限,除了实现一个拦截器之外不需要额外的代码,可以说非常优雅了。以axios为例,拦截器大概长这样:

axios.interceptors.request.use(function (config) {
 if(!has(config)){
 //验证不通过
  return Promise.reject({
   message: `no permission`
  });
 }
 return config;
});

但如果仅仅这样做权限控制,界面上将显示出所有的按钮,用户看到的按钮却不一定可以点击,这种体验我认为只能停留在理论层面,根本无法应用到实际产品中。请求控制可以作为整个控制体系的第二道防线,或某些特殊情况下的辅助手段,最终还是要回到按钮控制的思路上来。

那么怎样能尽可能方便的采集到每个按钮所需的权限呢?按钮和权限之间隔着两层东西,第一层是click回调,第二层是回调里的AJAX请求,不想人肉维护就得想办法突破这两层隔阂,让按钮和权限产生联系,按钮必然要绑定click事件,最理想的采集方式是在绑定事件的同时得到所需权限,让一切自然而然的发生,比如这样,

<btn v-do="Fn">按钮</btn>

如果Fn能以某种形式采集到内部的AJAX请求参数,并转化成权限信息传递出来就完美了,然而我没找到可行的方法,并且这种形式在应用上也存在缺陷,因为不一定每个操作按钮都会发起AJAX请求,比如编辑按钮本身并不会触发请求,真正触发请求的是另一个保存按钮,所以这个思路只是看起来很美。

那么退而求其次的做法是让按钮和请求联系起来,比如说按钮涉及一个名称为A的请求,那么我希望权限指令可以这样写,

<btn v-has="A" @click="Fn">按钮</btn>

比完美形态是差了不少,但起码不需要手动维护到'get,/resources'这个级别了,这里对A的实现可以有多种形式,比如A可以是一个包含两个属性的对象:

const A = {
 p: ['put,/menu/**'],
 r: params => {
  return axios.put(`/menu/${params.id}`, params)
 }
};

//用作权限:
<btn v-has="[A]" @click="Fn">按钮</btn>
//用作请求:
function Fn(){
  A.r().then((res) => {})
}

通常我们会将项目里所有的api放在一个api模块里集中管理,在写api时顺便就把权限给维护了,换来的是在组件界面里可以直接用请求名称来描述权限,而不需要来回奔波于界面和api模块之间,一定程度上实现了关注点分离,而且has指令还可以进一步做优化,例如参数只需要接收A,指令内部根据约定自动访问A.p来获取权限,还可以接收数组,允许多个权限联合校验。

后记

好了,这就是我对前端权限控制的一些实践和思考,如有不当欢迎指正。

最后吐槽一下Element-UI,真心难看。

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

Javascript 相关文章推荐
深入分析js中的constructor和prototype
Apr 07 Javascript
js/html光标定位的实现代码
Sep 23 Javascript
JavaScript正则表达式中的ignoreCase属性使用详解
Jun 16 Javascript
javascript实现tab切换特效
Nov 12 Javascript
Jquery-1.9.1源码分析系列(十一)之DOM操作
Nov 25 Javascript
JQuery datepicker 用法详解
Dec 25 Javascript
跨域请求的完美解决方法(JSONP, CORS)
Jun 12 Javascript
带有定位当前位置的百度地图前端web api实例代码
Jun 21 Javascript
Node.js 使用命令行工具检查更新
Jun 08 Javascript
React Native模块之Permissions权限申请的实例相机
Sep 28 Javascript
CountUp.js实现数字滚动增值效果
Oct 17 Javascript
package.json中homepage属性的作用详解
Mar 11 Javascript
jQuery动态添加.active 实现导航效果代码思路详解
Aug 29 #jQuery
laravel5.4+vue+element简单搭建的示例代码
Aug 29 #Javascript
详解从新建vue项目到引入组件Element的方法
Aug 29 #Javascript
jQuery实现base64前台加密解密功能详解
Aug 29 #jQuery
浅谈vuex之mutation和action的基本使用
Aug 29 #Javascript
vuex学习之Actions的用法详解
Aug 29 #Javascript
jquery在vue脚手架中的使用方式示例
Aug 29 #jQuery
You might like
默默小谈PHP&amp;MYSQL分页原理及实现
2007/01/02 PHP
PHP中使用匿名函数操作数据库的例子
2014/11/17 PHP
ThinkPHP控制器详解
2015/07/27 PHP
WordPress中自定义后台管理界面配色方案的小技巧
2015/12/29 PHP
laravel框架中间件 except 和 only 的用法示例
2019/07/12 PHP
你需要知道的JavsScript可以做什么?
2007/06/29 Javascript
Jquery Ajax学习实例 向页面发出请求,返回XML格式数据
2010/03/14 Javascript
利用location.hash实现跨域iframe自适应
2010/05/04 Javascript
jquery div 居中技巧应用介绍
2012/11/24 Javascript
JS上传图片前的限制包括(jpg jpg gif及大小高宽)等
2012/12/19 Javascript
浅析return false的正确使用
2013/11/04 Javascript
为Javascript中的String对象添加去除左右空格的方法(示例代码)
2013/11/30 Javascript
易操作的jQuery表单提示插件
2015/12/01 Javascript
js实现异步循环实现代码
2016/02/16 Javascript
浅谈Javascript数组(推荐)
2016/05/17 Javascript
jQuery Ajax页面局部加载方法汇总
2016/06/02 Javascript
对vue下点击事件传参和不传参的区别详解
2018/09/15 Javascript
模块化react-router配置方法详解
2019/06/03 Javascript
微信小程序实现收货地址左滑删除
2020/11/18 Javascript
vue悬浮可拖拽悬浮按钮的实例代码
2019/08/20 Javascript
jQuery实现移动端扭蛋机抽奖
2020/11/08 jQuery
[01:17:12]职来职往完美电竞专场
2014/09/18 DOTA
Python实现的最近最少使用算法
2015/07/10 Python
python中requests库session对象的妙用详解
2017/10/30 Python
Python实现的tcp端口检测操作示例
2018/07/24 Python
python3.4控制用户输入与输出的方法
2018/10/17 Python
python+selenium实现自动抢票功能实例代码
2018/11/23 Python
pycharm debug功能实现跳到循环末尾的方法
2018/11/29 Python
Python中logging.NullHandler 的使用教程
2018/11/29 Python
Python3.6 中的pyinstaller安装和使用教程
2020/03/16 Python
ITK 实现多张图像转成单个nii.gz或mha文件案例
2020/07/01 Python
python 解决Windows平台上路径有空格的问题
2020/11/10 Python
机电专业毕业生求职信
2013/10/27 职场文书
应届生污水处理求职信
2013/11/06 职场文书
菜篮子工程实施方案
2014/03/08 职场文书
旅游饭店管理专业自荐书
2014/06/28 职场文书