权限控制主要分为两块,认证(Authentication)与授权(Authorization)。认证之后确认了身份正确,业务系统就会进行授权,现在业界比较流行的模型就是RBAC(Role-Based Access Control)。RBAC包含为下面四个要素:用户、角色、权限、资源。用户是源头,资源是目标,用户绑定至角色,资源与权限关联,最终将角色与权限关联,就形成了比较完整灵活的权限控制模型。
资源是最终需要控制的标的物,但是我们在一个业务系统中要将哪些元素作为待控制的资源呢?我将系统中待控制的资源分为三类:

  1. URL访问资源(接口以及网页)
  2. 界面元素资源(增删改查导入导出的按钮,重要的业务数据展示与否等)
  3. 数据资源

现在业内普遍的实现方案实际上很粗放,就是单纯的“菜单控制”,通过菜单显示与否来达到控制权限的目的。
我仔细分析过,现在大家做的平台分为To C和To B两种:

  1. To C一般不会有太多的复杂权限控制,甚至大部分连菜单控制都不用,全部都可以访问。
  2. To B一般都不是开放的,只要做好认证关口,能够进入系统的只有内部员工。大部分企业内部的员工互联网知识有限,而且作为内部员工不敢对系统进行破坏性的尝试。

所以针对现在的情况,考虑成本与产出,大部分设计者也不愿意在权限上进行太多的研发力量。
菜单和界面元素一般都是由前端编码配合存储数据实现,URL访问资源的控制也有一些框架比如SpringSecurity,Shiro。
目前我还没有找到过数据权限控制的框架或者方法,所以自己整理了一份。

数据权限控制原理

数据权限控制最终的效果是会要求在同一个数据请求方法中,根据不同的权限返回不同的数据集,而且无需并且不能由研发编码控制。这样大家的第一想法应该就是AOP,拦截所有的底层方法,加入过滤条件。这样的方式兼容性较强,但是复杂程度也会更高。我们这套系统中,采用的是利用Mybatis的plugin机制,在底层SQL解析时替换增加过滤条件。
这样一套控制机制存在很明显的优缺点,首先缺点:

  1. 适用性有限,基于底层的Mybatis。
  2. 方言有限,针对了某种数据库(我们使用Mysql),而且由于需要在底层解析处理条件所以有可能造成不同的数据库不能兼容。当然Redis和NoSQL也无法限制。

当然,假如你现在就用Mybatis,而且数据库使用的是Mysql,这方面就没有太大影响了。

接下来说说优点:

  1. 减少了接口数量及接口复杂度。原本针对不同的角色,可能会区分不同的接口或者在接口实现时利用流程控制逻辑来区分不同的条件。有了数据权限控制,代码中只用写基本逻辑,权限过滤由底层机制自动处理。
  2. 提高了数据权限控制的灵活性。例如原本只有主管能查本部门下组织架构/订单数据,现在新增助理角色,能够查询本部门下组织架构,不能查询订单。这样的话普通的写法就需要调整逻辑控制,使用数据权限控制的话,直接修改配置就好。

数据权限实现

上一节就提及了实现原理,是基于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进行数据权限控制相关推荐

  1. java 数据 权限_Java如何利用Mybatis进行数据权限控制详解

    前言 权限控制主要分为两块,认证(Authentication)与授权(Authorization).认证之后确认了身份正确,业务系统就会进行授权,现在业界比较流行的模型就是RBAC(Role-Bas ...

  2. mybatis整合数据权限

    现在很多企业级应用都需要拦截数据权限, 只有配置了相应数据权限的人才能看到该数据 关于数据权限的实现, 个人想了两种实现方式 第一种是基于AOP, 配置相应的注解, 在切面中将数据权限的参数值强制设置 ...

  3. 基于SpringAOP实现数据权限控制

    基于SpringAOP实现数据权限控制 在此主要是实现对用户查询数据返回字段的控制.比如一个表格有A,B,C,D,E五列,用户U1只能查看A,B,C三列. 此文章讲述的内容并不能实现在查询时仅查询A, ...

  4. 实现权限控制_SpringCloud 微服务实现数据权限控制

    前章讲了如何进行用户权限验证<SpringCloud下的用户鉴权方案>,它是微服务下统一资源访问权限的控制,就像一道墙保护着SpringCloud集群下的各个业务应用服务.而本章要讲的是权 ...

  5. 实现数据权限控制的一种方法

    在企业管理系统中,常常有这样的要求:  1. 用户一般只能查看自己部门的数据  2. 可以设置用户可以查看哪些部门的数据  这种权限的控制,一般称为数据权限,与之对应的功能权限,则是系统中哪些功能可以 ...

  6. 天鸟技术中台-建设过程-日常经验2:通用参数和数据权限控制

    技术中台,服务于 无数个 独立的项目. 每个项目,单独对应1个App和appId. 查询和创建等接口,必须提供appId,区分是哪个app的数据. 而技术中台的平台管理端,是管理所有的数据,appId ...

  7. 后台管理系统2——数据权限控制

    文章目录 1 数据权限控制 1.1数据库的修改 1.2 后端代码的修改 1.3 前端代码的修改 1.4 Aside组件的修改 1 数据权限控制 通过不同的身份进行实现不同的功能. 1.1数据库的修改 ...

  8. Java数据库行列级数据权限控制_关于数据库表字段的数据权限设计

    吐槽:刚在同事的帮忙下,把maven工程成功导入到eclipse,期间遇到的最大问题就是安装eclipse插件,花费了其中大部分的时间 现在做的研发产品,遇到的一个新的需求是"控制外部系统对 ...

  9. PowerBI Report Server 集成,自定义身份验证,数据权限控制

    AD认证的集成模式 AD(或者Windows)认证是PBI的默认认证方式.在这种方式下,需要第三方应用也采用了AD的认证方式才可以正常的和PBI RS做集成.注意,这种方式要求第三方系统采用了AD的认 ...

最新文章

  1. 区块链相关论文研读2 - vChain,关于可验证的查询
  2. 第18章 多线程----线程同步
  3. 超低延迟直播架构解析
  4. GitHub上3天1W赞的程序员学习路线!入门进阶都非常实用
  5. 安装SQL2000 提示 以前的某个程序安装已在安装计算机上创建挂起的文件
  6. 协方差 之 随机变量间的协方差及向量之间的协方差之间的微妙的区别
  7. eclipse检测不到android的手机
  8. ORACLE数据库设置编码
  9. Android listview局部刷新
  10. HTML5开启浏览器桌面通知 Web Notification
  11. 学生信息管理系统(Java+JSP+MySQL)
  12. conda 设置清华镜像源_conda使用清华镜像源问题
  13. 开始讨厌现在这种生活
  14. dcloud wap2app上架应用宝过审方法
  15. Unity3d Ugui 23图集Sprite Atlas
  16. css3中的渐变色效果
  17. Linux Load Averages: Solving the Mystery
  18. java静态代码块和实例代码块
  19. c语言课程设计自动取款机,C语言课程设计………银行自动存取款机模拟.doc
  20. 向左转移测试需要团队的努力

热门文章

  1. CSS布局案例 5-51 折扇效果
  2. jupyter 更换主题
  3. 哪个蓝牙耳机性价比高?五款高性价比蓝牙耳机推荐
  4. 流畅web动画的十个法则
  5. 图像化转向名词解释_图形学名词解释2(转帖)
  6. Unity 物体旋转
  7. 自用分享丨45个自媒体必备工具素材网站
  8. 信息学奥赛一本通:2058:【例3.10】简单计算器
  9. 如何把图片转换为PDF文档
  10. 一劳永逸解决开始菜单运行总以管理员模式进行(cmd总是管理员模式)