mybatis自定义拦截器-数据权限过滤

   最近一段时间公司搞新项目,数据库orm选用了mybatis框架。使用一段时间mybaits后感觉比其他orm框架灵活好用,好处就不说了,网上一搜大把。本次主要讲下mybatis自定义拦截器功能的开发,通过拦截器可以解决项目中蛮多的问题,虽然很多功能不用拦截器也可以实现,但使用自定义拦截器实现功能从我角度至少以下优点(1)灵活,解耦(2)统一控制 ,减少开发工作量,不用散落到每个业务功能点去实现。

    一般业务系统项目都涉及到数据权限的控制,此次结合本项目记录下基于mybatis拦截器实现数据权限的过滤,因为项目用到mybatis-plus的分页插件,数据权限拦截过滤的时机也要控制好,在分页拦截器之前先拦截修改sql,不然会导致查询出来的数据同分页统计出来数量不一致。


拦截器基本知识


    Mybatis采用责任链模式,通过动态代理组织多个拦截器,通过这些拦截器可以改变mybatis的默认行为,编写自定义拦截器最好了解下它的原理,以便写出安全高效的插件。

 (1)拦截器均需要实现org.apache.ibatis.plugin.Interceptor 接口,对于自定义拦截器必须使用mybatis 提供的注解来指明我们要拦截的是四类中的哪一个类接口。

具体规则如下:

 a:Intercepts 标识我的类是一个拦截器

 b:Signature 则是指明我们的拦截器需要拦截哪一个接口的哪一个方法;type对应四类接口中的某一个,比如是 Executor;method对应接口中的哪类方法,比如 Executor 的 update 方法;args 对应接口中的哪一个方法,比如 Executor 中 query 因为重载原因,方法有多个,args 就是指明参数类型,从而确定是哪一个方法。

@Intercepts({

    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})

})

(2) mybatis 拦截器默认可拦截的类型四种,即四种接口类型 Executor、StatementHandler、ParameterHandler 和 ResultSetHandler,对于我们的自定义拦截器必须使用 mybatis 提供的注解来指明我们要拦截的是四类中的哪一个类接口。

(3)拦截器顺序:

 不同类型拦截器的顺序Executor -> ParameterHandler -> StatementHandler ->ResultSetHandler

  同类型的拦截器的不同对象拦截顺序则根据 mybatis 核心配置文件的配置位置,拦截顺序是 从上往下,在mybatis 核心配置文件中需要配置我们的 plugin 


数据权限过滤


   1.实现业务需求的数据过滤,在用户访问数据库时进行权限判断并改造sql,达到限制低权限用户访问数据的目的

   2.采用技术:mybatis拦截器,java自定义注解,反射,开源jsqlparser

   3.核心业务流程图


4.代码实现

(1)创建自定义注解

```

import java.lang.annotation.Documented;

import java.lang.annotation.ElementType;

import java.lang.annotation.Inherited;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

/**

* 数据权限注解

*

*/

@Documented

@Target( value = { ElementType.TYPE, ElementType.METHOD } )

@Retention( RetentionPolicy.RUNTIME )

@Inherited

public @interface DataAuth

{

/**

* 追加sql的方法名

* @return

*/

public String method() default "whereSql";

/**

* 表别名

* @return

*/

public String tableAlias() default "";

}

```

(2)mapper方法增加权限注解

```

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface TestMapper extends BaseMapper<Test> {

    /**

    * 增加权限注解

    *

    */

    @DataAuth(tableAlias = "o")

    List<TestEntity> listData(TestQuery testQuery);

}

```

(3)创建自定义拦截器

```

import net.sf.jsqlparser.expression.Expression;

import net.sf.jsqlparser.expression.Parenthesis;

import net.sf.jsqlparser.expression.operators.conditional.AndExpression;

import net.sf.jsqlparser.parser.CCJSqlParserManager;

import net.sf.jsqlparser.parser.CCJSqlParserUtil;

import net.sf.jsqlparser.statement.select.PlainSelect;

import net.sf.jsqlparser.statement.select.Select;

import org.apache.commons.lang3.StringUtils;

import org.apache.ibatis.executor.Executor;

import org.apache.ibatis.mapping.BoundSql;

import org.apache.ibatis.mapping.MappedStatement;

import org.apache.ibatis.mapping.SqlCommandType;

import org.apache.ibatis.mapping.SqlSource;

import org.apache.ibatis.plugin.Interceptor;

import org.apache.ibatis.plugin.Intercepts;

import org.apache.ibatis.plugin.Invocation;

import org.apache.ibatis.plugin.Plugin;

import org.apache.ibatis.plugin.Signature;

import org.apache.ibatis.reflection.DefaultReflectorFactory;

import org.apache.ibatis.reflection.MetaObject;

import org.apache.ibatis.reflection.factory.DefaultObjectFactory;

import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;

import org.apache.ibatis.session.ResultHandler;

import org.apache.ibatis.session.RowBounds;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.BeansException;

import org.springframework.context.ApplicationContext;

import org.springframework.context.ApplicationContextAware;

import org.springframework.stereotype.Component;

import java.io.StringReader;

import java.lang.reflect.Method;

import java.util.Map;

import java.util.Properties;

/**

* 数据权限拦截器

* 根据各个微服务,继承DataAuthService增加不同的where语句

*

*/

@Component

@Intercepts({@Signature(method = "query",type = Executor.class,args =  {

        MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}

    )

})

public class MybatisDataAuthInterceptor implements Interceptor,

    ApplicationContextAware {

    private final static Logger logger = LoggerFactory.getLogger(MybatisDataAuthInterceptor.class);

    private static ApplicationContext context;

    @Override

    public void setApplicationContext(ApplicationContext applicationContext)

        throws BeansException {

        context = applicationContext;

    }

    @Override

    public Object intercept(Invocation arg0) throws Throwable {

        MappedStatement mappedStatement = (MappedStatement) arg0.getArgs()[0];

        // 只对查询sql拦截

        if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {

            return arg0.proceed();

        }

        // String mSql = sql;

        // 注解逻辑判断 添加注解了才拦截追加

        Class<?> classType = Class.forName(mappedStatement.getId()

                                                          .substring(0,

                    mappedStatement.getId().lastIndexOf(".")));

        String mName = mappedStatement.getId()

                                      .substring(mappedStatement.getId()

                                                                .lastIndexOf(".") +

                1, mappedStatement.getId().length()); //

        for (Method method : classType.getDeclaredMethods()) {

            if (method.isAnnotationPresent(DataAuth.class) &&

                    mName.equals(method.getName())) {

                /**

                * 查找标识了该注解 的实现 类

                */

                Map<String, Object> beanMap = context.getBeansWithAnnotation(DataAuth.class);

                if ((beanMap != null) && (beanMap.entrySet().size() > 0)) {

                    for (Map.Entry<String, Object> entry : beanMap.entrySet()) {

                        DataAuth action = method.getAnnotation(DataAuth.class);

                        if (StringUtils.isEmpty(action.method())) {

                            break;

                        }

                        try {

                            Method md = entry.getValue().getClass()

                                            .getMethod(action.method(),

                                    new Class[] { String.class });

                            /**

                            * 反射获取业务 sql

                            */

                            String whereSql = (String) md.invoke(context.getBean(

                                        entry.getValue().getClass()),

                                    new Object[] { action.tableAlias() });

                            if (!StringUtils.isEmpty(whereSql) &&

                                    !"null".equalsIgnoreCase(whereSql)) {

                                Object parameter = null;

                                if (arg0.getArgs().length > 1) {

                                    parameter = arg0.getArgs()[1];

                                }

                                BoundSql boundSql = mappedStatement.getBoundSql(parameter);

                                MappedStatement newStatement = newMappedStatement(mappedStatement,

                                        new BoundSqlSqlSource(boundSql));

                                MetaObject msObject = MetaObject.forObject(newStatement,

                                        new DefaultObjectFactory(),

                                        new DefaultObjectWrapperFactory(),

                                        new DefaultReflectorFactor());

                                /**

                                * 通过JSqlParser解析 原有sql,追加sql条件

                                */

                                CCJSqlParserManager parserManager = new CCJSqlParserManager();

                                Select select = (Select) parserManager.parse(new StringReader(

                                            boundSql.getSql()));

                                PlainSelect selectBody = (PlainSelect) select.getSelectBody();

                                Expression whereExpression = CCJSqlParserUtil.parseCondExpression(whereSql);

                                selectBody.setWhere(new AndExpression(

                                        selectBody.getWhere(),

                                        new Parenthesis(whereExpression)));

                                /**

                                * 修改sql

                                */

                                msObject.setValue("sqlSource.boundSql.sql",

                                    selectBody.toString());

                                arg0.getArgs()[0] = newStatement;

                                logger.info("Interceptor sql:" +

                                    selectBody.toString());

                            }

                        } catch (Exception e) {

                            logger.error(null, e);

                        }

                        break;

                    }

                }

            }

        }

        return arg0.proceed();

    }

    private MappedStatement newMappedStatement(MappedStatement ms,

        SqlSource newSqlSource) {

        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(),

                ms.getId(), newSqlSource, ms.getSqlCommandType());

        builder.resource(ms.getResource());

        builder.fetchSize(ms.getFetchSize());

        builder.statementType(ms.getStatementType());

        builder.keyGenerator(ms.getKeyGenerator());

        if ((ms.getKeyProperties() != null) &&

                (ms.getKeyProperties().length != 0)) {

            StringBuilder keyProperties = new StringBuilder();

            for (String keyProperty : ms.getKeyProperties()) {

                keyProperties.append(keyProperty).append(",");

            }

            keyProperties.delete(keyProperties.length() - 1,

                keyProperties.length());

            builder.keyProperty(keyProperties.toString());

        }

        builder.timeout(ms.getTimeout());

        builder.parameterMap(ms.getParameterMap());

        builder.resultMaps(ms.getResultMaps());

        builder.resultSetType(ms.getResultSetType());

        builder.cache(ms.getCache());

        builder.flushCacheRequired(ms.isFlushCacheRequired());

        builder.useCache(ms.isUseCache());

        return builder.build();

    }

    /**

    * 当目标类是Executor类型时,才包装目标类,否者直接返回目标本身,减少目标被代理的次数

    */

    @Override

    public Object plugin(Object target) {

        if (target instanceof Executor) {

            return Plugin.wrap(target, this);

        }

        return target;

    }

    @Override

    public void setProperties(Properties arg0) {

        // TODO Auto-generated method stub

    }

    class BoundSqlSqlSource implements SqlSource {

        private BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql) {

            this.boundSql = boundSql;

        }

        @Override

        public BoundSql getBoundSql(Object parameterObject) {

            return boundSql;

        }

    }

}

```

(4)增加业务逻辑

```

import com.baomidou.mybatisplus.core.toolkit.StringUtils;

import com.winhong.wincloud.constant.RoleTypeJudge;

import com.winhong.wincore.async.ThreadLocalHolder;

import com.winhong.wincore.user.LoginUserHolder;

import com.winhong.wincore.user.UserInfo;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.stereotype.Service;

@Service

public abstract class AbstractDataAuthService {

    private static final Logger LOG = LoggerFactory.getLogger(AbstractDataAuthService.class);

    /**

    * 默认查询sql,根据角色不同追加不同业务查询条件

    *

    * @return

    */

    public String whereSql(String tableAlias) {

        if (!StringUtils.isEmpty(tableAlias)) {

            tableAlias = tableAlias + ".";

        }

        StringBuffer sql = new StringBuffer();

        //利用threadlocal获取用户角色信息

        UserInfo userInfo = LoginUserHolder.getUser();

        // 普通 用户

        if (RoleTypeJudge.isNormalUser(userInfo.getRoleTypeCode())) {

            sql.append(nomalUserSql(userInfo.getUserUuid(), tableAlias));

        }

        // 管理员

        else if (RoleTypeJudge.isManager(userInfo.getRoleTypeCode())) {

            sql.append(managerSql(tableAlias));

        } else {

        }

        return sql.toString();

    }

}

```

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容