文档章节

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

GMarshal
 GMarshal
发布于 2018/04/17 22:40
字数 1480
阅读 6408
收藏 146

数据权限管理中心

由于公司大部分项目都是使用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

博客

https://my.oschina.net/gmarshal/blog/1797026

欢迎关注我的个人微信订阅号:(据说这个头像程序猿专用)

输入图片说明

© 著作权归作者所有

GMarshal
粉丝 20
博文 63
码字总数 49721
作品 0
南京
程序员
私信 提问
加载中

评论(14)

一号男嘉宾
一号男嘉宾
你确定你看过shiro是怎么处理的?你确定session获取和threadlocal获取操作是一样的?我说tl可以,你说分布式不行。我说博主没说是分布式,你说session也可以。你是来刷存在感的么?
伊人枫
伊人枫

引用来自“一号男嘉宾”的评论

引用来自“伊人枫”的评论

引用来自“一号男嘉宾”的评论

获取当前登录用户,threadlocal可以完成
分布式下,你还用threadlocal?

博主文章里面说到了是在分布式下?
不是分布式,session就可以。
一号男嘉宾
一号男嘉宾

引用来自“伊人枫”的评论

引用来自“一号男嘉宾”的评论

获取当前登录用户,threadlocal可以完成
分布式下,你还用threadlocal?

博主文章里面说到了是在分布式下?
一号男嘉宾
一号男嘉宾
博主文章里面说到了是在分布式下?
伊人枫
伊人枫

引用来自“一号男嘉宾”的评论

获取当前登录用户,threadlocal可以完成
分布式下,你还用threadlocal?
chenjazz
chenjazz
如果原sql含有order by且数据量大,会有性能问题
Leon_wy
Leon_wy

引用来自“wangbo888”的评论

更新和删除怎么处理
目前哥们估计也是抛砖引玉,大家一起贡献
BabyMason
BabyMason

引用来自“一号男嘉宾”的评论

引用来自“蓝水晶飞机”的评论

想法挺好的。我目前都是业务层自己控制,这样的可以减少SQL拦截解析加工的成本。

业务层自己控制,效率更好,只是麻烦点。
应该大多数都是业务层控制较多吧,虽然知道有拦截器,但是基本很少用mybatis的。。。
行者__
行者__
面向对象思想 害人不浅 造出难点,然后解决难点……
一号男嘉宾
一号男嘉宾
获取当前登录用户,threadlocal可以完成
iBase4J-JAVA分布式Web系统

iBas4J项目简介 iBase4J是基于Java的开源框架搭建的分布式系统架构。 使用Maven对项目进行模块化管理,提高项目的易开发性、扩展性。 系统包括两个模块:系统管理模块、调度管理模块、Web展示...

iBase4J
2016/06/15
32
0
springmvc+mybatis +Jeesz 分布式架构

spring mvc Spring框架(框架即:编程注解+xml配置的方式)MVC是Spring框架的一大特征,Spring框架有三大特征(IOC(依赖注入),AOP(面向切面),MVC(建模M-视图V-控制器C)。框架一般用于...

愉快的鱼儿
2017/06/01
0
0
springmvc+mybatis +Jeesz 分布式架构

spring mvc Spring框架(框架即:编程注解+xml配置的方式)MVC是Spring框架的一大特征,Spring框架有三大特征(IOC(依赖注入),AOP(面向切面),MVC(建模M-视图V-控制器C)。框架一般用于...

愉快的鱼儿
2017/05/16
0
0
mofum/open-scope

open-scope 介绍 OpenScope是一种轻量级、易维护的数据权限的解决方案,它能处理比较复杂的权限操作逻辑。兼容操作权限Shiro等框架。 OpenScope提供了一种基于SQL的智能添加权限范围列的方案...

mofum
03/28
0
0
Spring-MyBatis-SQL拦截

一、应用场景 在项目中, 针对敏感数据等重要数据的列表查询,为避免数据泄露等, 需要针对数据添加权限。 场景:有三个账号:员工A、员工B、经理A;专员A、B 属于经理A的下级,且他们属于同...

ge洋
07/16
106
0

没有更多内容

加载失败,请刷新页面

加载更多

OpenStack 简介和几种安装方式总结

OpenStack :是一个由NASA和Rackspace合作研发并发起的,以Apache许可证授权的自由软件和开放源代码项目。项目目标是提供实施简单、可大规模扩展、丰富、标准统一的云计算管理平台。OpenSta...

小海bug
昨天
5
0
DDD(五)

1、引言 之前学习了解了DDD中实体这一概念,那么接下来需要了解的就是值对象、唯一标识。值对象,值就是数字1、2、3,字符串“1”,“2”,“3”,值时对象的特征,对象是一个事物的具体描述...

MrYuZixian
昨天
6
0
数据库中间件MyCat

什么是MyCat? 查看官网的介绍是这样说的 一个彻底开源的,面向企业应用开发的大数据库集群 支持事务、ACID、可以替代MySQL的加强版数据库 一个可以视为MySQL集群的企业级数据库,用来替代昂贵...

沉浮_
昨天
6
0
解决Mac下VSCode打开zsh乱码

1.乱码问题 iTerm2终端使用Zsh,并且配置Zsh主题,该主题主题需要安装字体来支持箭头效果,在iTerm2中设置这个字体,但是VSCode里这个箭头还是显示乱码。 iTerm2展示如下: VSCode展示如下: 2...

HelloDeveloper
昨天
7
0
常用物流快递单号查询接口种类及对接方法

目前快递查询接口有两种方式可以对接,一是和顺丰、圆通、中通、天天、韵达、德邦这些快递公司一一对接接口,二是和快递鸟这样第三方集成接口一次性对接多家常用快递。第一种耗费时间长,但是...

程序的小猿
昨天
10
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部