数据权限管理中心 - 基于mybatis拦截器实现


声明:本文转载自https://my.oschina.net/gmarshal/blog/1797026,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

数据权限管理中心

由于公司大部分项目都是使用mybatis,也是使用mybatis的拦截器进行分页处理,所以技术上也直接选择从拦截器入手

需求场景

第一种场景:行级数据处理

原sql:

select id,username,region from sys_user ; 

需要封装成:

select * from (     select id,username,region from sys_user  ) where 1=1 and region like “3210%"; 

解释

用户只能查询当前所属市以及下属地市数据 其中 like 部分也可以为动态参数(下面会讲到)

此场景还有以下情况:

# 判断 select * from (select id,username,region from sys_user ) where 1=1 and region != 320101; # 枚举 select * from (select id,username,region from sys_user ) where 1=1 and region in (320101,320102,320103); ... 

第二种场景:列级数据处理

原sql:

select id,username,region from sys_user ; 
  • 用户A可以看到 id,username,region
  • 用户B只能查看 id,username 的值,region的值没有权限查看。

应用流程图

输入图片说明

应用链路逻辑图

输入图片说明

技术实现

mybatis拦截器

在编写mybatis的拦截器之前,我们先来了解下mybaits的拦截目标方法

  • 1、Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

  • 2、ParameterHandler (getParameterObject, setParameters)

  • 3、StatementHandler (prepare, parameterize, batch, update, query)

  • 4、ResultSetHandler (handleResultSets, handleOutputParameters)

这里选择StatementHandler 的 prepare 方法作为sql执行之前的拦截进行sql封装,使用ResultSetHandler 的 handleResultSets 方法作为sql执行之后的结果拦截过滤。

sql执行前

PrepareInterceptor.java

/**  * mybatis数据权限拦截器 - prepare  * @author GaoYuan  * @date 2018/4/17 上午9:52  */ @Intercepts({         @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class,Integer.class }) }) @Component public class PrepareInterceptor implements Interceptor {     /** 日志 */     private static final Logger log = LoggerFactory.getLogger(PrepareInterceptor.class);      @Override     public Object plugin(Object target) {         return Plugin.wrap(target, this);     }      @Override     public void setProperties(Properties properties) {}      @Override     public Object intercept(Invocation invocation) throws Throwable {         if(log.isInfoEnabled()){             log.info("进入 PrepareInterceptor 拦截器...");         }         if(invocation.getTarget() instanceof RoutingStatementHandler) {             RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();             StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate");             //通过反射获取delegate父类BaseStatementHandler的mappedStatement属性             MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement");             //千万不能用下面注释的这个方法,会造成对象丢失,以致转换失败             //MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];             PermissionAop permissionAop = PermissionUtils.getPermissionByDelegate(mappedStatement);             if(permissionAop == null){                 if(log.isInfoEnabled()){                     log.info("数据权限放行...");                 }                 return invocation.proceed();             }             if(log.isInfoEnabled()){                 log.info("数据权限处理【拼接SQL】...");             }             BoundSql boundSql = delegate.getBoundSql();             ReflectUtil.setFieldValue(boundSql, "sql", permissionSql(boundSql.getSql()));         }         return invocation.proceed();     }      /**      * 权限sql包装      * @author GaoYuan      * @date 2018/4/17 上午9:51      */     protected String permissionSql(String sql) {         StringBuilder sbSql = new StringBuilder(sql);         String userMethodPath = PermissionConfig.getConfig("permission.client.userid.method");         //当前登录人         String userId = (String)ReflectUtil.reflectByPath(userMethodPath);         //如果用户为 1 则只能查询第一条         if("1".equals(userId)){             //sbSql = sbSql.append(" limit 1 ");             //如果有动态参数 regionCd             if(true){                 String premission_param = "regionCd";                 //select * from (select id,name,region_cd from sys_exam ) where region_cd like '${}%'                 String methodPath = PermissionConfig.getConfig("permission.client.params." + premission_param);                 String regionCd = (String)ReflectUtil.reflectByPath(methodPath);                 sbSql = new StringBuilder("select * from (").append(sbSql).append(" ) s where s.regionCd like concat("+ regionCd +",'%')  ");             }          }         return sbSql.toString();     } } 

sql执行后

ResultInterceptor.java

/**  * mybatis数据权限拦截器 - handleResultSets  * 对结果集进行过滤  * @author GaoYuan  * @date 2018/4/17 上午9:52  */ @Intercepts({         @Signature(type = ResultSetHandler.class, method = "handleResultSets", args={Statement.class}) }) @Component public class ResultInterceptor implements Interceptor {     /** 日志 */     private static final Logger log = LoggerFactory.getLogger(ResultInterceptor.class);      @Override     public Object plugin(Object target) {         return Plugin.wrap(target, this);     }      @Override     public void setProperties(Properties properties) {}      @Override     public Object intercept(Invocation invocation) throws Throwable {         if(log.isInfoEnabled()){             log.info("进入 ResultInterceptor 拦截器...");         }         ResultSetHandler resultSetHandler1 = (ResultSetHandler) invocation.getTarget();         //通过java反射获得mappedStatement属性值         //可以获得mybatis里的resultype         MappedStatement mappedStatement = (MappedStatement)ReflectUtil.getFieldValue(resultSetHandler1, "mappedStatement");         //获取切面对象         PermissionAop permissionAop = PermissionUtils.getPermissionByDelegate(mappedStatement);          //执行请求方法,并将所得结果保存到result中         Object result = invocation.proceed();         if(permissionAop != null) {             if (result instanceof ArrayList) {                 ArrayList resultList = (ArrayList) result;                 for (int i = 0; i < resultList.size(); i++) {                     Object oi = resultList.get(i);                     Class c = oi.getClass();                     Class[] types = {String.class};                     Method method = c.getMethod("setRegionCd", types);                     // 调用obj对象的 method 方法                     method.invoke(oi, "");                     if(log.isInfoEnabled()){                         log.info("数据权限处理【过滤结果】...");                     }                 }             }         }         return result;     } } 

其中 PermissionAop 为 dao 层自定义切面,用于开关控制是否启用数据权限过滤。

难点

  1. 如何在拦截器获取dao层注解内容;
  2. 如何获取当前登录人标识;
  3. 如何传递动态参数;
  4. 需要考虑到与sql分页的优先级。

解答

拦截器获取dao层注解

不同方法的拦截器获取方法稍微有所区别,具体在上面的 PrepareInterceptor.javaResultInterceptor.java 代码中自行查看。

获取当前登录人标识

由于不同框架或者不同项目,获取当天登录人的方法可能不一样,那么就只能通过配置的方式动态将获取当前登录人的方法传递给权限中心。 配置文件中添加:

# 客户端获取当前登录人标识 permission.client.userid.method=com.raising.sc.permission.example.util.UserUtils.getUserId 

然后利用Java反射机制,触发getUserId( )方法。

传递动态参数

比如用户A只能查询自己单位以及下属单位的所有数据; 配置中心配置的where部分的sql如下:

org_cd like concat(${orgCd},'%') 

然后通过PrepareInterceptor.java读取到以上sql,并且通过数据库或者配置文件中设置的参数【orgCd】相关联的方法(类似获取当前登录人标识的方式),提前在权限参数(orgCd)配置好对应的方法路径、参数值类型、返回值类型等。

配置文件或者数据库获取到 orgCd 对应的方法路径:

com.raising.sc.permission.example.util.UserUtils.getRegionCdByUserId 

当然,现在这样只是简单的动态参数,其余的还需要后续的开发,这里只是最简单的尝试。

拓展

从产品的角度来说,此模块需要有三个部分组成:

1、foruo-permission-admin 数据权限管理平台 2、foruo-permission-server 数据权限服务端(提供权限相关接口) 3、foruo-permission-client 数据权限客户端(封装API)

在结合 应用链路逻辑图 即可完成此模块内容。

涉及知识点:

  • Mybatis拦截器
  • Java反射机制

项目源码

码云:https://gitee.com/gmarshal/foruo-sc-permission

本文发表于2018年04月18日 22:38
(c)注:本文转载自https://my.oschina.net/gmarshal/blog/1797026,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 2055 讨论 0 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

这世界真好,吃野东西也要留出这条命来看看

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1