数据权限管理中心 - 基于mybatis拦截器实现
数据权限管理中心
由于公司大部分项目都是使用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 层自定义切面,用于开关控制是否启用数据权限过滤。
难点
如何在拦截器获取dao层注解内容;
如何获取当前登录人标识;
如何传递动态参数;
需要考虑到与sql分页的优先级。
解答
拦截器获取dao层注解
不同方法的拦截器获取方法稍微有所区别,具体在上面的 PrepareInterceptor.java 与 ResultInterceptor.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
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
王者荣耀高并发背后的故事
“王者荣耀”是一款国民级手机游戏,用户体量巨大,而且一直保持着较高的更新频率。这种业务场景下,突发也变得非常频繁,然而业务体验是至关重要的,使用CDN必不可少。类似地,经常有带宽突发的场景,比如新闻爆点视频、大型直播活动、热门影视剧上线、热门游戏等应用发布。同时,由于家庭带宽和移动网络的快速升级,突发带宽量级越来越大,经常达到Tb级,甚至10Tb 。如何快速、低成本地保障业务突发,成为CDN的一大挑战。 2007年,腾讯自建CDN启用,接入了第一个业务腾讯网。到现在CDN带宽量级,从最早的数十Gb,发展到现在的数十Tb;单业务的带宽也越来越大,大部分业务常量带宽在几百Gb,部分突发业务达到了10Tb。网络的快速升级,移动用户爆发式增长,以及视频类业务包括点播和直播的兴起,使得业务突发越来越频繁,突发带宽越来越高,对CDN的要求也越来越高。 自建CDN得益于腾讯业务的蓬勃发展,先后支持了游戏下载、流媒体视频加速、春节红包等腾讯内部业务;2014年腾讯将CDN全面能力开放,成为腾讯云CDN产品,除承载内部业务外,也开始接入第三方客户,比如快手点播、斗鱼直播等。以上各种业务都有突发场景,也有...
- 下一篇
Java高并发之从零到放弃
前言 本篇主要讲解如何去优化锁机制或者克服多线程因为锁可导致性能下降的问题 ThreadLocal线程变量 有这样一个场景,前面是一大桶水,10个人去喝水,为了保证线程安全,我们要在杯子上加锁导致大家轮着排队喝水,因为加了锁的杯子是同步的,只能有一个人拿着这个唯一的杯子喝水这样子大家都喝完一杯水需要很长的时间如果我们给每个人分发一个杯子呢?是不是每人喝到水的时间缩小到了十分之一 多线程并发也是一个道理在每个Thread中都有自己的数据存放空间(ThreadLocalMap)而ThreadLocal就是在当前线程的存放空间中存放数据下面这个例子,在每个线程中存放一个arraylist,而不是大家去公用一个arraylist publicclassThreadLocalTest{ publicstaticThreadLocalthreadLocal=newThreadLocal(); publicstaticArrayListlist=newArrayList(); publicstaticclassDemoimplementsRunnable{ privateinti; publicDe...
相关文章
文章评论
共有0条评论来说两句吧...