2021年7月1日星期四

Vue前端访问控制方案

通过dom元素的class来标识与访问控制相关的dom元素,目的是确定需要进行权限控制的组件范围,再通过dom的id对应后端权限设置的功能节点的domKey,从而实现前后端联动的权限控制。

1、前端访问控制的常规处理方法

  前端访问控制,一般针对界面元素dom element进行可见属性或enable属性进行控制,有权限的,相关元素可见或使能;没权限的,相关元素不可见或失能。这样用户可以明确哪些是无权访问的。可见属性要比使能属性更广泛,这是每个dom元素都有的属性。

  当然前端控制仅仅是整体访问控制的一部分,后端还需要进一步针对接口访问进行鉴权。因为通过编辑浏览器的界面元素的属性,可以绕过前端控制。

  在Vue中,也有通过控制路由来实现访问控制的,但没有控制界面元素的情况下,用户体验不是很好。

  本文给出了Vue框架下前端访问控制的整体方案。

2、总体方案

  在用户登录时,或权限变更时,后端通过接口将权限树发给前端。为了减少不必要的数据传输,后端发出的权限树仅包括有权限的功能项,即前端收到的权限树的各个节点都是有权限的功能项。

  权限树节点的数据部分即为功能项的权限信息,包括两个关键字段:url和domKey。url是后端自己使用,在AOP鉴权切面类中,拦截非法的接口访问。domKey是给前端使用的,即dom element的id值,domKey的确定需要前后端协商一致,不能搞错。

  domKey在同一个路径上,不允许重复;不同路径,允许重复。所谓路径,是从根节点开始,到该节点的一系列节点组成的树杈。当然,没有必要的话,domKey最好不重复。同一个界面视图范围的各子节点的domKey也不允许重复。

  前端本地存储用户token和权限树JSON字符串,如果本地这个存储信息存在,重新打开浏览器,可以免登录。(仅本地token有效,不能完全保证token真的有效,如后端重启服务器、token过期等导致token失效,前端通过HTTP访问时,仍然会跳到登录页面)。

  登录成功后,将token和权限树JSON字符串保存到本地存储。

  权限发生变更时,通过response拦截器,检查有无附加信息,如有需要,更新token和权限树JSON字符串。

  前端开发一个权限树的管理的js文件,用于权限树JSON对象的访问,权限树JSON字符串被转换成权限树JSON对象。

  开发前端页面vue文件时,需要进行权限控制的dom element,使用下列属性:

 id="相关domKey"

  通过class来标识该界面元素是与访问控制相关的,目的是确定需要进行权限控制的组件范围,id即为该功能项对应的domKey。

  然后,使用一个公共权限设置方法,来统一处理权限相关的界面元素。

  由于Vue的组件style,可以有scoped属性设置,此时,在App.vue中,就不能访问到相关dom element的class,局部式样渲染后,在外部被改写,因此,在scoped限制的情况下,需要在scoped起作用的Vue组件中,也要调用公共权限设置方法。另外,scoped的限制,恰好使得相同domKey的节点,可以通过上级节点domKey来加以区分。这样,就用统一的方法,实现了前端页码的访问控制。

3、方案实现

3.1、功能项的表结构定义

DROP TABLE IF EXISTS `function_tree`;CREATE TABLE `function_tree`( `func_id`  INT(11)  NOT NULL DEFAULT 0 COMMENT '功能ID', `func_name`  VARCHAR(100) NOT NULL DEFAULT '' COMMENT '功能名称', `parent_id`  INT(11)  NOT NULL DEFAULT 0 COMMENT '父功能ID', `level`   TINYINT(4) NOT NULL DEFAULT 0 COMMENT '功能所在层级', `order_no`	 INT(11)  NOT NULL DEFAULT 0 COMMENT '显示顺序', `url`			 VARCHAR(80) NOT NULL DEFAULT '' COMMENT '访问接口url', `dom_key`  VARCHAR(80) NOT NULL DEFAULT '' COMMENT 'dom对象的id', `remark`  VARCHAR(200) NOT NULL DEFAULT '' COMMENT '备注', -- 记录操作信息 `operator_name` VARCHAR(80) NOT NULL DEFAULT '' COMMENT '操作人账号', `delete_flag` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '记录删除标记,1-已删除', `create_time` DATETIME(3) NOT NULL DEFAULT NOW(3) COMMENT '创建时间', `update_time` DATETIME(3)   DEFAULT NULL ON UPDATE NOW(3) COMMENT '更新时间', PRIMARY KEY (`func_id`)) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='功能表';

  如有需要,可以增加icon字段,用于前端树节点的显示。

3.2、后端权限树的输出

  后端在登录成功后,给前端发送token和权限树JSON字符串。

  关于树节点的生成,可参阅:Java通用树结构数据管理---https://www.cnblogs.com/alabo1999/p/14928380.html。里面有关于权限树的例子。

  为了方便前端管理,这里修改权限树的输出,将根节点也一并输出到前端。

  在管理员修改用户权限后,动态权限更新,可通过附加信息,给前端发送token和权限树JSON字符串。参阅:Spring Boot动态权限变更实现的整体方案---https://www.cnblogs.com/alabo1999/p/14948914.html。

3.3、前端本地缓存

  vue项目中,新建/src/store目录,创建inde.js文件。代码如下:

import Vue from 'vue';import Vuex from 'vuex';Vue.use(Vuex); const store = new Vuex.Store({ state: { // 存储token token: localStorage.getItem('token') ? localStorage.getItem('token') : '', // 存储权限树 rights: localStorage.getItem('rights') ? localStorage.getItem('rights') : '' }, mutations: { // 修改token,并将token存入localStorage changeLogin (state, user) {  if(user.token){  state.token = user.token;  localStorage.setItem('token', user.token);  }  if (user.rights){  state.rights = user.rights;  localStorage.setItem('rights', user.rights);  } } }}); export default store;

3.4、创建权限管理模块

  vue项目中,新建/src/common目录,创建treeNode.js文件。代码如下:

/** * 处理树结构数据,这里主要指功能权限树 * 权限树的结构如下: * [ * { *  nodeData:{ *   funcId:1,  //功能ID *   funcName:"", //功能名称 *   parentId:0,  //父节点ID *   level:1,  //功能所在层级 *   orderNo:2,  //显示顺序 *   url:"",   //访问接口url *   domKey:""  //dom对象的id *  }, *  children:[ *   nodeData:{...}, *   children:[...] *  ] * }, * { *  nodeData:{...}, *  children:[...] * } * ] */ var TreeNode = {  //功能树 rightsTree:null,  /**  * 将权限树的JSON字符串加载到树对象上  * @param {权限树的JSON字符串} rights  */ loadData(rights){  //将缓存的JSON字符串,转为JSON对象,为一级树节点的数组  var treeNode = JSON.parse(rights);    return treeNode; }, /**  * 在给定树上,找到上级domkey为superDomkey的给定domKey的树节点  * 不同子树如果存在子节点domKey重复的情况,也可以区分  * @param {给定树节点} rightsTree  * @param {上级的domkey} superDomkey  * @param {树节点的domkey} domKey  */ lookupNodeByDomkeys(rightsTree,superDomkey,domKey){  var node = null;  var superNode = null;  //先寻找superDomkey  if(superDomkey != ""){   //如果上级对象的domkey非空   superNode = this.lookupNodeByDomkey(rightsTree,superDomkey);  }  if (superNode != null){   //如果上级节点非空,或已找到,则在子树上搜索,可加快搜索速度,并且可避免子节点domKey重复的情况   node = this.lookupNodeByDomkey(superNode,domKey);  }else{   node = this.lookupNodeByDomkey(rightsTree,domKey);  }  return node; }, /**  * 在给定的子树中,搜索指定domKey的树节点  * @param {子树} rightsTree  * @param {domkey} domKey  */ lookupNodeByDomkey(rightsTree,domKey){  var node = null;  var functionInfo = rightsTree.nodeData;  //先查找自身的数据  if (functionInfo.domKey == domKey){   //如果找到,则返回   return rightsTree;  }  //搜索子节点  for (var i = 0; i < rightsTree.children.length; i++){   var item = rightsTree.children[i];   node = this.lookupNodeByDomkey(item,domKey);   if (node != null){    break;   }  }  return node; }  } export default TreeNode;

  如果domKey确保唯一的话,使用Map可能是访问效率更高的方案。这里还是使用树型结构来管理权限树。

3.5、创建公共方法模块

  vue项目中,在/src/common目录下,创建commonFuncs.js文件。代码如下:

import TreeNode from './treeNode.js'var commonFuncs = { checkRights(superDomkey){  //先加载权限树 if (TreeNode.rightsTree == null){  let rights = localStorage.getItem('rights');  if (rights === null || rights === ''){  //没有权限树  return;  }  //加载权限树  TreeNode.rightsTree = TreeNode.loadData(rights); } //获取class包含permissions的所有dom对象 var elements = document.getElementsByClassName('permissions'); for(var i = 0; i < elements.length; i++){  var element = elements[i];  if (element.id != undefined)  {  var node = null;  //如果对象有id,检查权限  if (superDomkey == null || superDomkey == undefined){   //如果未指定上级domkey,直接查找   node = TreeNode.lookupNodeByDomkey(TreeNode.rightsTree,element.id);  }else{   //指定上级domkey   node = TreeNode.lookupNodeByDomkeys(TreeNode.rightsTree,superDomkey,element.id)  }  if (node != null && node != undefined){   //包含节点   if (element.style.display == "none"){   element.style.display = "";   }   console.log('has rights :'+element.id);  }else{   element.style.display="none";    console.log('has not rights :'+element.id);  }    }    } }};export default commonFuncs;

  checkRights方法,参数为superDomkey,即指定上级节点的domKey,允许为空或空串,相当于不指定。其查找当前页面或scoped范围的文档中,class名称包含permissions的所有dom元素。取得dom的id,即功能节点的domKey,如果在权限树中存在对应节点,则表示有权限;否则表示无权限。(注意:前端的权限树都是有权限的功能节点)。

3.6、修改main.js

  修改main.js文件,使得公共模块生效。代码如下:

// The Vue build version to load with the `import` command// (runtime-only or standalone) has been set in webpack.base.conf with an alias.import Vue from 'vue'import App from './App'import router from './router'import store from './store'import ElementUI from 'element-ui'import 'element-ui/lib/theme-chalk/index.css'import md5 from 'js-md5';import axios from 'axios'import VueAxios from 'vue-axios'import TreeNode_ from './common/treeNode.js'import CommonFuncs_ from './common/commonFuncs.js'import instance_ from './api/index.js'import global_ from '../config/global.js'Vue.use(VueAxios,axios)Vue.prototype.$md5 = md5Vue.prototype.TreeNode = TreeNode_Vue.prototype.$baseUrl = process.env.API_ROOTVue.prototype.instance = instance_ //axios实例Vue.prototype.global = global_Vue.prototype.commonFuncs = CommonFuncs_Vue.use(ElementUI)Vue.config.productionTip = false/* eslint-disable no-new */var vue = new Vue({ el: '#app', router, store, components: { App }, template: '<App/>', render:h=>h(App) })export default vue

  引入了commonFuncs和TreeNode全局对象,可以在vue文件中使用。

3.7、组件示例

  侧边导航栏,与权限控制相关,可以作为示例。文件为Left.vue,代码如下:

<template> <div > <el-menu :default-openeds="['1']" >  <el-submenu index="1">  <el-menu-item-group >   <el-menu-item index="1-1">   <router-link tag="li" to="/home" exact-active-    id="homeMenu" active->    <i ></i>首页   </router-link>   </el-menu-item>   <el-submenu index="1-2" id="userManagementMain">   <template slot="title" ><i ></i>用户管理</template>   <el-menu-item index="1-2-1" id="userManagementSub">    <router-link tag="li" to="/userManagement">     <i ></i>用户管理    </router-link>   </el-menu-item>   <el-menu-item index="1-2-2" id="changePassword">    <router-link tag="li" to="/changePassword">     <i ></i>修改密码    </router-link>   </el-menu-item>      </el-submenu>   <el-menu-item index="1-3" id="questionnaireManagement">   <router-link tag="li" to="/questionnaireManagement">    <i ></i>问卷内容管理   </router-link>   </el-menu-item>   <el-submenu index="1-4" id="issueManagementMain">   <template slot="title"><i ></i>问卷发布管理</template>   <el-menu-item index="1-4-1" id="issueManagementSub">    <router-link tag="li" to="/issueManagement">     <i ></i>发布问卷查询    </router-link>   </el-menu-item>   <el-menu-item index="1-4-2" id="issueTaskQuery">    <router-link tag="li" to="/issueTaskQuery">     <i ></i>发布任务查询    </router-link>   </el-menu-item>   </el-submenu>   <el-menu-item index="1-5" id="answerSheetManagement">   <router-link tag="li" to="/answerSheetManagement">    <i ></i>答卷管理   </router-link>   </el-menu-item>       </el-menu-item-group>  </el-submenu> </el-menu> </div></template><style> /* 去掉右边框 */ .el-menu { border-right: none; } .el-submenu { background-color: rgb(231, 235, 220) ; } </style>

  注意那些: id="XXX"的dom元素,基本都是el-menu-item。这里,将scoped去掉了,因为菜单项,目前只有侧边导航栏在使用。

3.7、修改App.vue

  App.vue,作为应用页面组件的总成,在里面进行总的权限控制。代码如下:

<template> <div id="app"> <!-- 其他页 --> <el-container v-if="$route.meta.keepAlive">  <!-- 无头部导航栏 -->  <el-container>  <el-aside :>   <!-- 侧边栏 -->   <keep-alive>   <left></left>   </keep-alive>  </el-aside>  <el-main>   <!-- Body -->   <router-view></router-view>  </el-main>  </el-container>  <!-- 无足部 --> </el-container>  <!-- 登录页 --> <router-view v-if="!$route.meta.keepAlive"></router-view> </div></template><script>import left from './components/Left.vue'export default { name: 'App', components: { left: left }, data(){ return {  collpaseWidth:200 } }, mounted:function(){ this.commonFuncs.checkRights(); }, methods: {  } }</script><style>#app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px;}</style>

  在页码加载时,调用commonFuncs.checkRights()方法,进行权限控制。

3.8、测试一下

3.8.1、获取权限树数据

  登录成功后,后端输出的权限树数据如下:

{	rights = {		"nodeData": {			"funcId": 0,			"funcName": "root",			"parentId": -1,			"level": 0,			"orderNo": 0,			"url": "",			"domKey": ""		},		"children": [{			"nodeData": {				"funcId": 1,				"funcName": "用户管理一级菜单",				"parentId": 0,				"level": 1,				"orderNo": 0,				"url": "",				"domKey": "userManagementMain"			},			"children": [{				"nodeData": {					"funcId": 3,					"funcName": "修改密码",					"parentId": 1,					"level": 2,					"orderNo": 1,					"url": "/userMan/changePassword",					"domKey": "changePassword"				},				"children": []			}]		}, {			"nodeData": {				"funcId": 10,				"funcName": "问卷内容管理一级菜单",				"parentId": 0,				"level": 1,				"orderNo": 1,				"url": "",				"domKey": "questionnaireManagement"			},			"children": [{				"nodeData": {					"funcId": 11,					"funcName": "新增问卷",					"parentId": 10,					"level": 2,					"orderNo": 0,					"url": "/questionnaireMan/addQuestionnaire",					"domKey": "addQuestionnaire"				},				"children": []			}, {				"nodeData": {					"funcId": 12,					"funcName": "编辑问卷",					"parentId": 10,					"level": 2,					"orderNo": 1,					"url": "/questionnaireMan/editQuestionnaire",					"domKey": "editQuestionnaire"				},				"children": []			}, {				"nodeData": {					"funcId": 13,					"funcName": "查询问卷",					"parentId": 10,					"level": 2,					"orderNo": 2,					"url": "/questionnaireMan/queryQuestionnaires",					"domKey": "queryQuestionnaire"				},				"children": []			}, {				"nodeData": {					"funcId": 14,					"funcName": "复制新建问卷",					"parentId": 10,					"level": 2,					"orderNo": 3,					"url": "",					"domKey": "copyAddQuestionnaire"				},				"children": []			}, {				"nodeData": {					"funcId": 15,					"funcName": "浏览问卷",					"parentId": 10,					"level": 2,					"orderNo": 4,					"url": "/questionnaireMan/previewQuestionnaire",					"domKey": "browseQuestionnaire"				},				"children": []			}, {				"nodeData": {					"funcId": 16,					"funcName": "提交审核",					"parentId": 10,					"level": 2,					"orderNo": 5,					"url": "/questionnaireMan/submitAduit",					"domKey": "submitAudit"				},				"children": []			}, {				"nodeData": {					"funcId": 18,					"funcName": "作废问卷",					"parentId": 10,					"level": 2,					"orderNo": 7,					"url": "/questionnaireMan/cancelQuestionnaire",					"domKey": "cancelQuestionnaire"				},				"children": []			}]		}, {			"nodeData": {				"funcId": 20,				"funcName": "问卷发布管理一级菜单",				"parentId": 0,				"level": 1,				"orderNo": 2,				"url": "",				"domKey": "issueManagementMain"			},			"children": [{				"nodeData": {					"funcId": 21,					"funcName": "发布管理二级菜单",					"parentId": 20,					"level": 2,					"orderNo": 0,					"url": "",					"domKey": "issueManagementSub"				},				"children": []			}, {				"nodeData": {					"funcId": 22,					"funcName": "发布任务查询",					"parentId": 20,					"level": 2,					"orderNo": 1,					"url": "",					"domKey": "issueTaskQuery"				},				"children": []			}]		}, {			"nodeData": {				"funcId": 40,				"funcName": "答卷管理一级菜单",				"parentId": 0,				"level": 1,				"orderNo": 3,				"url": "",				"domKey": "answerSheetManagement"			},			"children": [{				"nodeData": {					"funcId": 41,					"funcName": "查询答卷记录",					"parentId": 40,					"level": 2,					"orderNo": 0,					"url": "/answerSheetMan/queryAnswerTask",					"domKey": "queryAnswerSheet"				},				"children": []			}, {				"nodeData": {					"funcId": 42,					"funcName": "回收记录明细",					"parentId": 40,					"level": 2,					"orderNo": 1,					"url": "/answerSheetMan/getAnswerSubmitDetail",					"domKey": "recoveryDetail"				},				"children": []			}, {				"nodeData": {					"funcId": 43,					"funcName": "答......

原文转载:http://www.shaoqun.com/a/840139.html

跨境电商:https://www.ikjzd.com/

卖家精灵:https://www.ikjzd.com/w/532

转运中国:https://www.ikjzd.com/w/1549

adore:https://www.ikjzd.com/w/2202


通过dom元素的class来标识与访问控制相关的dom元素,目的是确定需要进行权限控制的组件范围,再通过dom的id对应后端权限设置的功能节点的domKey,从而实现前后端联动的权限控制。1、前端访问控制的常规处理方法  前端访问控制,一般针对界面元素domelement进行可见属性或enable属性进行控制,有权限的,相关元素可见或使能;没权限的,相关元素不可见或失能。这样用户可以明确哪些是无权
亚马逊发布最新产品标题政策 针对非媒体类产品!:https://www.ikjzd.com/articles/109476
独立站适合亚马逊新手卖家吗?应该如何开启?:https://www.ikjzd.com/articles/109477
亚马逊是如何判定卖家操控review,手段都在这里...:https://www.ikjzd.com/articles/109478
实操运营:Shopify独立站的引流最强合集(下):https://www.ikjzd.com/articles/109479
美女用手扒开自己下面 把她压在桌上进进出出:http://lady.shaoqun.com/a/247404.html
好想同时被两个男人舔b 老公和朋友一起来干我:http://lady.shaoqun.com/a/274580.html
长途汽车上艳遇故事 口述坐长途大巴遇见美好的经历:http://www.30bags.com/m/a/250656.html
几位领导在办公室玩我 把她压在办公桌上进进出出:http://www.30bags.com/m/a/249746.html
不要轻举妄动:一时冲动会发生男女性交:http://lady.shaoqun.com/a/394663.html
WISH宣布重新开放旧金山总部,提供灵活的工作选择:https://www.ikjzd.com/articles/146288
土豪是我们最喜欢的对象!如何认识十二星座的土豪:http://lady.shaoqun.com/a/394664.html
11岁小孩被培训机构老板性侵10次,处女膜完全破了!:http://lady.shaoqun.com/a/394665.html

没有评论:

发表评论