基于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 相关文章推荐
jquery 图片 上一张 下一张 链接效果(续篇)
Apr 20 Javascript
JqGrid web打印实现代码
May 31 Javascript
JS连连看源码完美注释版(推荐)
Dec 09 Javascript
Jquery实现Div上下移动示例
Apr 23 Javascript
js实现兼容IE、Firefox的图片缩放代码
Dec 08 Javascript
浅析Javascript ES6中的原生Promise
Aug 25 Javascript
jquery实现刷新随机变化样式特效(tag标签样式)
Feb 03 Javascript
bootstrap 下拉多选框进行多选传值问题代码分析
Feb 14 Javascript
JS实现根据密码长度显示安全条功能
Mar 08 Javascript
详解Vue.js基于$.ajax获取数据并与组件的data绑定
May 26 Javascript
express + jwt + postMan验证实现持久化登录
Jun 05 Javascript
JS实现压缩上传图片base64长度功能
Dec 03 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
基于Laravel实现的用户动态模块开发
2017/09/21 PHP
PHP实现带进度条的Ajax文件上传功能示例
2019/07/02 PHP
TP5框架实现自定义分页样式的方法示例
2020/04/05 PHP
js判断输入是否为正整数、浮点数等数字的函数代码
2010/11/17 Javascript
最短的IE判断代码
2011/03/13 Javascript
js获取IFRAME当前的URL的方法
2013/11/13 Javascript
JavaScript继承基础讲解(原型链、借用构造函数、混合模式、原型式继承、寄生式继承、寄生组合式继承)
2014/08/16 Javascript
jQuery实现返回顶部功能适合不支持js的浏览器
2014/08/19 Javascript
JavaScript学习笔记之Cookie对象
2015/01/22 Javascript
js实现格式化金额,字符,时间的方法
2015/02/26 Javascript
js实现发送验证码后的倒计时功能
2015/05/28 Javascript
ES6中非常实用的新特性介绍
2016/03/10 Javascript
JavaScript实现自动切换图片代码
2016/10/11 Javascript
bootstrap table使用入门基本用法
2017/05/24 Javascript
vue项目中使用tinymce编辑器的步骤详解
2018/09/11 Javascript
js抽奖转盘实现方法分析
2020/05/16 Javascript
Python中字符串的修改及传参详解
2016/11/30 Python
Python hexstring-list-str之间的转换方法
2019/06/12 Python
Python class的继承方法代码实例
2020/02/14 Python
浅谈pytorch torch.backends.cudnn设置作用
2020/02/20 Python
Python接口开发实现步骤详解
2020/04/26 Python
Python Socket TCP双端聊天功能实现过程详解
2020/06/15 Python
html5的新增的标签和废除的标签简要概述
2013/02/20 HTML / CSS
普天C++笔试题
2016/03/20 面试题
介绍一下Python中webbrowser的用法
2013/05/07 面试题
行政专员工作职责
2013/12/22 职场文书
社区端午节活动方案
2014/01/28 职场文书
学生思想表现的评语
2014/01/30 职场文书
清明节扫墓活动方案
2014/03/02 职场文书
文体活动总结范文
2014/05/05 职场文书
党的群众路线教育实践活动个人承诺书
2014/05/22 职场文书
实现中国梦思想汇报2014
2014/09/13 职场文书
网吧管理制度范本
2015/08/05 职场文书
2016大学生形势与政策心得体会
2016/01/12 职场文书
小学一年级数学教学反思
2016/02/16 职场文书
解决pytorch读取自制数据集出现过的问题
2021/05/31 Python