前言
权限控制主要分为两块,认证(authentication)与授权(authorization)。认证之后确认了身份正确,业务系统就会进行授权,现在业界比较流行的模型就是rbac(role-based access control)。rbac包含为下面四个要素:用户、角色、权限、资源。用户是源头,资源是目标,用户绑定至角色,资源与权限关联,最终将角色与权限关联,就形成了比较完整灵活的权限控制模型。
资源是最终需要控制的标的物,但是我们在一个业务系统中要将哪些元素作为待控制的资源呢?我将系统中待控制的资源分为三类:
现在业内普遍的实现方案实际上很粗放,就是单纯的“菜单控制”,通过菜单显示与否来达到控制权限的目的。
我仔细分析过,现在大家做的平台分为to c和to b两种:
所以针对现在的情况,考虑成本与产出,大部分设计者也不愿意在权限上进行太多的研发力量。
菜单和界面元素一般都是由前端编码配合存储数据实现,url访问资源的控制也有一些框架比如springsecurity,shiro。
目前我还没有找到过数据权限控制的框架或者方法,所以自己整理了一份。
数据权限控制原理
数据权限控制最终的效果是会要求在同一个数据请求方法中,根据不同的权限返回不同的数据集,而且无需并且不能由研发编码控制。这样大家的第一想法应该就是aop,拦截所有的底层方法,加入过滤条件。这样的方式兼容性较强,但是复杂程度也会更高。我们这套系统中,采用的是利用mybatis的plugin机制,在底层sql解析时替换增加过滤条件。
这样一套控制机制存在很明显的优缺点,首先缺点:
当然,假如你现在就用mybatis,而且数据库使用的是mysql,这方面就没有太大影响了。
接下来说说优点:
数据权限实现
上一节就提及了实现原理,是基于mybatis的plugins)实现。
mybatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,mybatis 允许使用插件来拦截的方法调用包括:
executor (update, query, flushstatements, commit, rollback, gettransaction, close, isclosed)
parameterhandler (getparameterobject, setparameters)
resultsethandler (handleresultsets, handleoutputparameters)
statementhandler (prepare, parameterize, batch, update, query)
mybatis的插件机制目前比较出名的实现应该就是pagehelper项目了,在做这个实现的时候也参考了pagehelper项目的实现方式。所以权限控制插件的类命名为permissionhelper。
机制是依托于mybatis的plugins机制,实际sql处理的时候基于jsqlparser这个包。
设计中包含两个类,一个是保存角色与权限的实体类命名为permissionrule,一个是根据实体变更底层sql语句的主体方法类permissionhelper。
首先来看下permissionrule的结构:
public class permissionrule { private static final log log = logfactory.getlog(permissionrule.class); /** * codename<br> * 适用角色列表<br> * 格式如: ,rolea,roleb, */ private string roles; /** * codevalue<br> * 主实体,多表联合 * 格式如: ,systemcode,user, */ private string fromentity; /** * codedesc<br> * 过滤表达式字段, <br> * <code>{uid}</code>会自动替换为当前用户的userid<br> * <code>{me}</code> main entity 主实体名称 * <code>{me.a}</code> main entity alias 主实体别名 * 格式如: * <ul> * <li>userid = {uid}</li> * <li>(userid = {uid} and authtype > 3)</li> * <li>((userid = {uid} and authtype) > 3 or (dept in (select dept from depts where manager.id = {uid})))</li> * </ul> */ private string exps; /** * codeshowname<br> * 规则说明 */ private string rulecomment; }
看完这个结构,基本能够理解设计的思路了。数据结构中保存如下几个字段:
核心流程
系统启动时,首先从数据库加载出所有的规则。底层利用插件机制来拦截所有的查询语句,进入查询拦截方法后,首先根据当前用户的权限列表筛选出permissionrule列表,然后循环列表中的规则,对语句中符合实体列表的表进行条件增加,最终生成处理后的sql语句,退出拦截器,mybatis执行处理后sql并返回结果。
讲完permissionrule,再来看看permissionhelper,首先是头:
@intercepts({@signature(type = executor.class, method = "update", args = {mappedstatement.class, object.class}), @signature(type = executor.class, method = "query", args = {mappedstatement.class, object.class, rowbounds.class, resulthandler.class})}) public class permissionhelper implements interceptor { }
头部只是标准的mybatis拦截器写法,注解中的signature决定了你的代码对哪些方法拦截,update实际上针对修改(update)、删除(delete)生效,query是对查询(select)生效。
下面给出针对select注入查询条件限制的完整代码:
private string processselectsql(string sql, list<permissionrule> rules, userdefaultzimpl principal) { try { string replacesql = null; select select = (select) ccjsqlparserutil.parse(sql); plainselect selectbody = (plainselect) select.getselectbody(); string maintable = null; if (selectbody.getfromitem() instanceof table) { maintable = ((table) selectbody.getfromitem()).getname().replace("`", ""); } else if (selectbody.getfromitem() instanceof subselect) { replacesql = processselectsql(((subselect) selectbody.getfromitem()).getselectbody().tostring(), rules, principal); } if (!validutil.isempty(replacesql)) { sql = sql.replace(((subselect) selectbody.getfromitem()).getselectbody().tostring(), replacesql); } string maintablealias = maintable; try { maintablealias = selectbody.getfromitem().getalias().getname(); } catch (exception e) { log.debug("当前sql中, " + maintable + " 没有设置别名"); } string condexpr = null; permissionrule realruls = null; for (permissionrule rule : rules) { for (object rolestr : principal.getroles()) { if (rule.getroles().indexof("," + rolestr + ",") != -1) { if (rule.getfromentity().indexof("," + maintable + ",") != -1) { // 若主表匹配规则主体,则直接使用本规则 realruls = rule; condexpr = rule.getexps().replace("{uid}", userdefaultutil.getuserid().tostring()).replace("{bid}", userdefaultutil.getbusinessid().tostring()).replace("{me}", maintable).replace("{me.a}", maintablealias); if (selectbody.getwhere() == null) { selectbody.setwhere(ccjsqlparserutil.parsecondexpression(condexpr)); } else { andexpression and = new andexpression(selectbody.getwhere(), ccjsqlparserutil.parsecondexpression(condexpr)); selectbody.setwhere(and); } } try { string jointable = null; string jointablealias = null; for (join j : selectbody.getjoins()) { if (rule.getfromentity().indexof("," + ((table) j.getrightitem()).getname() + ",") != -1) { // 当主表不能匹配时,匹配所有join,使用符合条件的第一个表的规则。 realruls = rule; jointable = ((table) j.getrightitem()).getname(); jointablealias = j.getrightitem().getalias().getname(); condexpr = rule.getexps().replace("{uid}", userdefaultutil.getuserid().tostring()).replace("{bid}", userdefaultutil.getbusinessid().tostring()).replace("{me}", jointable).replace("{me.a}", jointablealias); if (j.getonexpression() == null) { j.setonexpression(ccjsqlparserutil.parsecondexpression(condexpr)); } else { andexpression and = new andexpression(j.getonexpression(), ccjsqlparserutil.parsecondexpression(condexpr)); j.setonexpression(and); } } } } catch (exception e) { log.debug("当前sql没有join的部分!"); } } } } if (realruls == null) return sql; // 没有合适规则直接退出。 if (sql.indexof("limit ?,?") != -1 && select.tostring().indexof("limit ? offset ?") != -1) { sql = select.tostring().replace("limit ? offset ?", "limit ?,?"); } else { sql = select.tostring(); } } catch (jsqlparserexception e) { log.error("change sql error .", e); } return sql; }
重点思路
重点其实就在于sql的解析和条件注入,使用开源项目jsqlparser。
结束语
想要达到无感知的数据权限控制,只有机制控制这么一条路。本文选择的是通过底层拦截sql语句,并且针对对应表注入条件语句这么一种做法。应该是非常经济的做法,只是基于文本处理,不会给系统带来太大的负担,而且能够达到理想中的效果。大家也可以提出其他的见解和思路。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。
如对本文有疑问, 点击进行留言回复!!
[杭电多校2020]第一场 1004 Distinct Sub-palindromes
Swift -- 将本地生成的UIImage进行持久化保存(存到文件中fileManager.createFile)
网友评论