基于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 相关文章推荐
Javascript实现关联数据(Linked Data)查询及注意细节
Feb 22 Javascript
js添加table的行和列 具体实现方法
Jul 22 Javascript
Jquery的hover方法让鼠标经过li时背景变色
Sep 06 Javascript
用js正确判断用户名cookie是否存在的方法
Jan 28 Javascript
js document.write()使用介绍
Feb 21 Javascript
JavaScipt中栈的实现方法
Feb 17 Javascript
ionic实现滑动的三种方式
Aug 27 Javascript
jquery事件与绑定事件
Mar 16 Javascript
jquery如何实现点击空白处隐藏元素
Dec 05 jQuery
Vue.Draggable拖拽功能的配置使用方法
Jul 29 Javascript
Vue批量图片显示时遇到的路径被解析问题
Mar 28 Javascript
js实现前端界面导航栏下拉列表
Aug 27 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
smarty内置函数section的用法
2015/01/22 PHP
ThinkPHP3.2.2实现持久登录(记住我)功能的方法
2016/05/16 PHP
用JTrackBar实现的模拟苹果风格的滚动条
2007/08/06 Javascript
JQuery页面图片切换和新闻列表滚动效果的具体实现
2013/09/26 Javascript
手机平板等移动端适配跳转URL的js代码
2014/01/25 Javascript
javascript在IE下trim函数无法使用的解决方法
2014/09/12 Javascript
jQuery中prop()方法用法实例
2015/01/05 Javascript
js图片模糊切换显示特效的方法
2015/02/17 Javascript
js实现仿网易点击弹出提示同时背景变暗效果
2015/08/13 Javascript
JS清除文本框内容离开在恢复及鼠标离开文本框时触发js的方法
2016/01/12 Javascript
js+flash实现的5图变换效果广告代码(附演示与demo源码下载)
2016/04/01 Javascript
JavaScript判断微信浏览器实例代码
2016/06/13 Javascript
利用angular.copy取消变量的双向绑定与解析
2016/11/25 Javascript
懒加载实现的分页&amp;&amp;网站footer自适应
2016/12/21 Javascript
JS正则表达式判断有效数实例代码
2017/03/13 Javascript
vue2.0 和 animate.css的结合使用
2017/12/12 Javascript
解决vue中修改了数据但视图无法更新的情况
2018/08/27 Javascript
在Vue 中使用Typescript的示例代码
2018/09/10 Javascript
[46:09]2014 DOTA2华西杯精英邀请赛 5 25 LGD VS VG第三场
2014/05/26 DOTA
[04:50]2019DOTA2高校联赛秋季赛四强集锦
2019/12/27 DOTA
python调用新浪微博API项目实践
2014/07/28 Python
Python中数字以及算数运算符的相关使用
2015/10/12 Python
对pandas进行数据预处理的实例讲解
2018/04/20 Python
在Python中居然可以定义两个同名通参数的函数
2019/01/31 Python
解决Django Static内容不能加载显示的问题
2019/07/28 Python
解决pycharm同一目录下无法import其他文件
2020/02/12 Python
Selenium自动化测试工具使用方法汇总
2020/06/12 Python
Python自动巡检H3C交换机实现过程解析
2020/08/14 Python
一家专门做特卖的网站:唯品会
2016/10/09 全球购物
英国知名小木屋定制网站:Tiger Sheds
2020/03/06 全球购物
客户代表自我评价范例
2013/09/24 职场文书
致跳远、跳高运动员广播稿
2014/01/09 职场文书
优秀乡村医生先进事迹材料
2014/08/23 职场文书
道歉情书大全
2015/05/12 职场文书
Spring Boot 启动、停止、重启、状态脚本
2021/06/26 Java/Android
Python中基础数据类型 set集合知识点总结
2021/08/02 Python