Java利用Mybatis进行数据权限控制
权限控制主要分为两块,认证(Authentication)与授权(Authorization)。认证之后确认了身份正确,业务系统就会进行授权,现在业界比较流行的模型就是RBAC(Role-Based Access Control)。RBAC包含为下面四个要素:用户、角色、权限、资源。用户是源头,资源是目标,用户绑定至角色,资源与权限关联,最终将角色与权限关联,就形成了比较完整灵活的权限控制模型。
资源是最终需要控制的标的物,但是我们在一个业务系统中要将哪些元素作为待控制的资源呢?我将系统中待控制的资源分为三类:
- URL访问资源(接口以及网页)
- 界面元素资源(增删改查导入导出的按钮,重要的业务数据展示与否等)
- 数据资源
现在业内普遍的实现方案实际上很粗放,就是单纯的“菜单控制”,通过菜单显示与否来达到控制权限的目的。
我仔细分析过,现在大家做的平台分为To C和To B两种:
- To C一般不会有太多的复杂权限控制,甚至大部分连菜单控制都不用,全部都可以访问。
- To B一般都不是开放的,只要做好认证关口,能够进入系统的只有内部员工。大部分企业内部的员工互联网知识有限,而且作为内部员工不敢对系统进行破坏性的尝试。
所以针对现在的情况,考虑成本与产出,大部分设计者也不愿意在权限上进行太多的研发力量。
菜单和界面元素一般都是由前端编码配合存储数据实现,URL访问资源的控制也有一些框架比如SpringSecurity,Shiro。
目前我还没有找到过数据权限控制的框架或者方法,所以自己整理了一份。
数据权限控制原理
数据权限控制最终的效果是会要求在同一个数据请求方法中,根据不同的权限返回不同的数据集,而且无需并且不能由研发编码控制。这样大家的第一想法应该就是AOP,拦截所有的底层方法,加入过滤条件。这样的方式兼容性较强,但是复杂程度也会更高。我们这套系统中,采用的是利用Mybatis的plugin机制,在底层SQL解析时替换增加过滤条件。
这样一套控制机制存在很明显的优缺点,首先缺点:
- 适用性有限,基于底层的Mybatis。
- 方言有限,针对了某种数据库(我们使用Mysql),而且由于需要在底层解析处理条件所以有可能造成不同的数据库不能兼容。当然Redis和NoSQL也无法限制。
当然,假如你现在就用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;
}
看完这个结构,基本能够理解设计的思路了。数据结构中保存如下几个字段:
- 角色列表:需要使用此规则的角色,可以多个,使用英文逗号隔开。
- 实体列表:对应的规则应用的实体(这里指的是表结构中的表名,可能你的实体是驼峰而数据库是蛇形,所以这里要放蛇形那个),可以多个,使用英文逗号隔开。
- 表达式:表达式就是数据权限控制的核心了。简单的说这里的表达式就是一段SQL语句,其中设置了一些可替换值,底层会用对应运行时的变量替换对应内容,从而达到增加条件的效果。
- 规则说明:单纯的一个说明字段。
核心流程
系统启动时,首先从数据库加载出所有的规则。底层利用插件机制来拦截所有的查询语句,进入查询拦截方法后,首先根据当前用户的权限列表筛选出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。
- 解析出MainTable和JoinTable。from之后跟着的称为MainTable,join之后跟着的称为JoinTable。这两个就是我们PermissionRule需要匹配的表名,PermissionRule::fromEntity字段。
- 解析出MainTable的where和JoinTable的on后面的条件。使用and连接原本的条件和待注入的条件,PermissionRule::exps字段。
- 使用当前登录的用户信息(放在缓存中),替换条件表达式中的值。
- 某些情况需要忽略权限,可以考虑使用ThreadLocal(单机)/Redis(集群)来控制。
结束语
想要达到无感知的数据权限控制,只有机制控制这么一条路。本文选择的是通过底层拦截Sql语句,并且针对对应表注入条件语句这么一种做法。应该是非常经济的做法,只是基于文本处理,不会给系统带来太大的负担,而且能够达到理想中的效果。大家也可以提出其他的见解和思路。
Java利用Mybatis进行数据权限控制的更多相关文章
- Java之封装与访问权限控制(一)
目录 Java之封装与访问权限控制(一) 封装的概念 访问控制符 属性私有化 Java之封装与访问权限控制(一) 对于封装的概念,我总觉得自己还是挺了解的,但是真要我说,还真说不出个啥来.我只能默默地 ...
- Java面试题03-访问权限控制
Java面试题03-访问权限控制 1. Java中的包主要是为了防止类文件命名冲突以及方便进行代码组织和管理,因此采用域名倒置的方式来进行命名: 2. Java解释器的运行过程:首先找到环境变量CLA ...
- Java之封装与访问权限控制(二)
目录 Java之封装与访问权限控制(二) 包:库单元 import import static Java常用包 Java之封装与访问权限控制(二) 访问权限控制是具体实现的隐藏,是封装性的一部分体现. ...
- SpringCloud微服务实战——搭建企业级开发框架(二十八):扩展MybatisPlus插件DataPermissionInterceptor实现数据权限控制
一套完整的系统权限需要支持功能权限和数据权限,前面介绍了系统通过RBAC的权限模型来实现功能的权限控制,这里我们来介绍,通过扩展Mybatis-Plus的插件DataPermissionInterce ...
- Java编程思想之六访问权限控制
访问控制(或隐藏具体实现)与"最初的实现并不恰当"有关. 访问权限控制的等级,从最大权限到最小权限依次为:public,protected,包访问权限(没有关键字)和private ...
- java利用poi导出数据到excel
背景: 上一篇写到利用jtds连接数据库获取对应的数据,本篇写怎样用poi将数据到处到excel中,此程序为Application 正文: 第三方poi jar包:poi驱动包下载 代码片段: /** ...
- java基础-类成员访问权限控制
一 前言 这篇文章是很基础的一文,没多大深度,对于开发人员必然是熟练于心.本篇文章的主题是为什么java要设置类成员访问级别?其原因也很简单,就是为了面向对象的封装特性:将类成员使用不同的访问级别控制 ...
- JAVA 利用反射自定义数据层框架
之前的随笔一直都在介绍c#,主要公司最近的业务都是做桌面程序,那么目前c#中的WPF肯定是我做桌面程序的不二之选,做了半年的WPF,也基本摸清了c#写代码的套路和规则(本人之前是两年多的JAVA开发者 ...
- java中使用二进制进行权限控制
基本概念 package test; publicclass Rights { publicstaticvoid main(String[] args) { int a=1; // 001 状态a i ...
随机推荐
- centos7 中安装 mysql5.6 的过程
前提是Centos的环境是好的,并且相关的软件包已经安装好. 1.创建用户,并修改创建的数据目录的属主 [root@bogon ~]# useradd -M mysql -s /sbin/nologi ...
- 主DNS服务-反向解析
上篇说了主DNS正向解析 当中是有个小问题的,什么问题呢? 试问当我们输入wwww或ww或更多w的时候它还能解析出来吗? 或者不输入w的时候还能解析吗? 上篇没有定义是解析不了的,怎么定义呢?很简单, ...
- Re:从零开始的Linux之路(文件权限)
基于 Red Hat Enterprise Linux 7.5 或者 CentOS 7.4 基本概念 Linux最核心的一个概念就是:Linux里面任何东西都可以被视为一个文件,包括系统本身(说到底L ...
- vue-ssr 文档备注
https://ssr.vuejs.org/zh/universal.html 基本用法 通过vue-server-renderer插件的createRenderer方法创建一个renderer,再调 ...
- JS 绘制心形线
JS 绘制心形线 <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> < ...
- 【curl】【php】curl报错,错误代码77,CURLE_SSL_CACERT_BADFILE (77)解决方法
CURLE_SSL_CACERT_BADFILE (77) - 读取 SSL CA 证书时遇到问题(可能是路径错误或访问权限问题) 在微信接口相关开发时容易出现此问题 这一般是因为服务更新了相关的软件 ...
- 强制停止及删除(卸载)Windows服务
1. 安装服务: CMD 打开命令行窗口:C:\> 运行:"C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe& ...
- Chrome 开发者工具(DevTools)中所有快捷方式列表(已整理)
Chrome 开发者工具(DevTools)中所有快捷方式列表(已整理) 前言 Chrome DevTools提供了一些内置的快捷键,开发者利用这些快捷键可以节省常工作中很多日的开发时间.下面列出了每 ...
- [转]python开发_shelve_完整版
''' python中的shelve模块,可以提供一些简单的数据操作 他和python中的dbm很相似. 区别如下: 都是以键值对的形式保存数据,不过在shelve模块中, key必须为字符串,而值可 ...
- vim 第三章 插入模式
vim 第三章 插入模式 在普通模式下可以删除 复制 及粘贴的命令 在插入模式下也存在以中方便快捷的方式 能够粘贴寄存器中文本 两种方式来插入键盘上不存在的非常用字符 替换模式 ...