数据权限管理中心

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

相关内容推荐:http://www.roncoo.com/course/list.html?courseName=mybatis

数据权限管理中心 - 基于mybatis拦截器实现的更多相关文章

  1. 基于mybatis拦截器分表实现

    1.拦截器简介 MyBatis提供了一种插件(plugin)的功能,但其实这是拦截器功能.基于这个拦截器我们可以选择在这些被拦截的方法执行前后加上某些逻辑或者在执行这些被拦截的方法时执行自己的逻辑. ...

  2. 基于Spring和Mybatis拦截器实现数据库操作读写分离

    首先需要配置好数据库的主从同步: 上一篇文章中有写到:https://www.cnblogs.com/xuyiqing/p/10647133.html 为什么要进行读写分离呢? 通常的Web应用大多数 ...

  3. Mybatis拦截器介绍

    拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法.Mybatis拦截器设计的一个初 ...

  4. 通过spring抽象路由数据源+MyBatis拦截器实现数据库自动读写分离

    前言 之前使用的读写分离的方案是在mybatis中配置两个数据源,然后生成两个不同的SqlSessionTemplate然后手动去识别执行sql语句是操作主库还是从库.如下图所示: 好处是,你可以人为 ...

  5. spring boot 实现mybatis拦截器

    spring boot 实现mybatis拦截器 项目是个报表系统,服务端是简单的Java web架构,直接在请求参数里面加了个query id参数,就是mybatis mapper的query id ...

  6. Mybatis拦截器介绍及分页插件

    1.1    目录 1.1 目录 1.2 前言 1.3 Interceptor接口 1.4 注册拦截器 1.5 Mybatis可拦截的方法 1.6 利用拦截器进行分页 1.2     前言 拦截器的一 ...

  7. 玩转SpringBoot之整合Mybatis拦截器对数据库水平分表

    利用Mybatis拦截器对数据库水平分表 需求描述 当数据量比较多时,放在一个表中的时候会影响查询效率:或者数据的时效性只是当月有效的时候:这时我们就会涉及到数据库的分表操作了.当然,你也可以使用比较 ...

  8. Mybatis拦截器实现原理深度分析

    1.拦截器简介 拦截器可以说使我们平时开发经常用到的技术了,Spring AOP.Mybatis自定义插件原理都是基于拦截器实现的,而拦截器又是以动态代理为基础实现的,每个框架对拦截器的实现不完全相同 ...

  9. Mybatis拦截器 mysql load data local 内存流处理

    Mybatis 拦截器不做解释了,用过的基本都知道,这里用load data local主要是应对大批量数据的处理,提高性能,也支持事务回滚,且不影响其他的DML操作,当然这个操作不要涉及到当前所lo ...

随机推荐

  1. Spring邮件发送2

    前言:上一篇博文讲解了邮件发送的基础用法(数据是写死的),然而在实际开发中,大多数情况下邮件内容都是根据业务来动态生成的.所以在此篇博文中,我们将讲解邮件发送携带数据的几种方案. 一.解析自定义占位符 ...

  2. Python 黑客相关电子资源和书籍推荐

    原创 2017-06-03 玄魂工作室 玄魂工作室 继续上一次的Python编程入门的资源推荐,本次为大家推荐的是Python网络安全相关的资源和书籍. 在去年的双11送书的时候,其实送过几本Pyth ...

  3. JS的if和switch

    var aa=parseInt(prompt("请输入你的年龄")); //定义输入 if(aa<18){ //输出小于18,返回值少年 alert("少年&quo ...

  4. ssh_maven之controller层开发

    我们已经完成了前两层的开发,现在 只剩下我们的controller层了,对于这一层,我们需要创建一个动作类CustomerAction,另外就是我们的strutss.xml以及我们的applicati ...

  5. Excel as a Service —— Excel 开发居然可以这么玩

    前言 据不完全统计,全世界使用Excel作为电子表格和数据处理的用户数以十亿计,这不仅得益于它的使用简便,同时还因为它内置了很多强大的函数,结合你的想象力可以编写出各种公式,并可快速根据数据生成图表和 ...

  6. python--socket套接字/TCP

    socket套接字/TCP 一 客户端/服务器架构 C/S架构,包括 硬件C/S架构(打印机) 软件C/S 架构(web服务) C/S架构的软件(软件属于应用层)是基于网络进行通信的 Server端要 ...

  7. 竞赛基础篇---部分和问题(DFS)

    问题链接:http://acm.nyist.edu.cn/JudgeOnline/problem.php?pid=1058 描述 给定整数a1.a2........an,判断是否可以从中选出若干数,使 ...

  8. selenium用法详解

    selenium用法详解 selenium主要是用来做自动化测试,支持多种浏览器,爬虫中主要用来解决JavaScript渲染问题. 模拟浏览器进行网页加载,当requests,urllib无法正常获取 ...

  9. Python selenium 三种等待方式详解

    1. 强制等待第一种也是最简单粗暴的一种办法就是强制等待sleep(xx),强制让闪电侠等xx时间,不管凹凸曼能不能跟上速度,还是已经提前到了,都必须等xx时间.看代码: # -*- coding: ...

  10. 确保 PHP 应用程序的安全 -- 不能违反的四条安全规则

    规则 1:绝不要信任外部数据或输入 关于 Web 应用程序安全性,必须认识到的第一件事是不应该信任外部数据.外部数据(outside data) 包括不是由程序员在 PHP 代码中直接输入的任何数据. ...