人生苦短,不如养狗
一、问题描述--动态查询条件
背景
文章的背景是一位同事想要使用Mybatis动态设置查询条件,但是这个过程中使用"$"却无法进行相应查询引发的问题。当时闲鱼正在拥抱Mybatis-Plus,所以顺带做了一个对比。
问题
本文主要介绍的是在项目使用中对于动态传入where和order by条件引发的问题进行的对Mybatis和Mybatis-Plus的对比。
项目中使用的是Mybatis,其中xml文件中一个查询语句是进行如下编写的:
order by #{condition}
在动态传入条件的时候使用了“#”,当然不适用“$”的目的就是为了防止SQL注入,但是实际使用的时候就会出现如下的问题:
order by ?
在日志中你可以看到传入的condition条件变成了"?",这是怎么回事?明明已经传了值,但是为什么在运行时会认为是没有值呢?
二、问题分析
"$"和"#"的区别
对于这个问题,我们先来看下"$"和"#"的区别。
在Mybatis中我们进行数据的传入会使用两种方式,一种是"#",一种是"$"。在使用中可能大部分都知道前一种可以防止SQL注入,而后一种不行,但是具体原因去没有进行深究。现在我们来分析一下这两种传值的不同。分析之前先来补充一个重要的知识点,Mybatis对于动态SQL的处理分为两个阶段,第一个阶段是动态解析,将动态SQL解析为一个BoundSql对象,第二个阶段是预编译。
首先我们来看下"$"传值,类似下面的语句:
select * from user where name = ${name}
Mybatis在遇到"$"符号的时候,会自动认为不需要进行任何处理,只需要简单纯粹的变量替换,所以在动态解析阶段就会进行变量替换,这也是导致SQL注入的重要原因。在动态解析之后,这条SQL会被处理为如下语句:
select * from user where name = 'zhangsan'
再来看下"#"传值,还是使用上面的语句:
select * from user where name = #{name}
和"$"不同的是,Mybatis在遇到"#"符号时会将其解析为一个JDBC预编译语句的参数标记符,简单来说,就是会使用preparestatement来进行SQL处理,使用preparestatement会将传入的参数与SQL进行分开处理,只有在DBMS完成SQL指令的编译之后才会套用参数运行(这一步中的编译操作由JDBC的SQL预编译进行处理,DBMS无需再进行SQL编译就可以直接执行)。
上面的语句经过动态编译之后会呈现如下结果:
select * from user where name = ?
一个#{}被解析为一个参数占位符"?",需要注意的是,使用SQL占位符来进行字符串变量替换的时候会带单引号'',这也是上面动态条件为什么使用"#"会出现SQL语法错误的原因。对于"#",变量替换是发生在DBMS中。
三、问题解决
1.使用Mybatis尝试解决
搞明白了出现上面问题原因,应该如何解决呢?其实很简单,我们传入类似表名或者字段名这些字段时使用的是字符串,此时像上面一样使用"#",在DBMS中运行时就出现如下情况:
order by 'condition'
此时就会出现SQL运行时错误,而在日志中打印时会将动态解析是的BoundSql对象打印出来,也就是:
order by ?
为了解决"#"的问题,我们可以使用"$",这样在动态解析时就会进行变量替换,而不会使用SQL占位符来进行替换。但是这样就会出现SQL注入问题。这时我想到了Mybatis不行的话,不如试一试Mybatis-Plus。
2.使用Mybatis-Plus尝试解决
在Mybatis-Plus中我们可以选择和Mybatis一样使用动态SQL语句,也可以使用EntityWrapper对象来组装查询条件,其中orderBy可以进行如下拼接:
EntityWrapper<MqdeliverChannelEntity> wrapper = new EntityWrapper<>();
wrapper.eq("article_id","test_01");
wrapper.orderBy("gmt_create and 0<>(select count(*) from admin)");
// and 0<>(select count(*) from admin) 该语句是用来测试SQL注入
很不幸,试验失败了。使用wrapper对象的orderBy方法可以进行动态传值,但是仍然无法阻止SQL注入。执行结果如下图展示:

看到这个结果后,我又去看了一遍源码,确实这个是作为字段直接进行SQL语句拼接,这个操作等效于使用"$"。但是我依然不死心,又跑到github上去咨询了一下作者,但是作者进行了如下回复:

好吧,是在下输了。那么就只能使用一种Low的方式解决了--使用枚举类。至于枚举类怎么写,大家自由发挥,我就不献丑了。
四、总结
经过对比,发现无论使用Mybatis还是Mybatis-Plus都是无法避免SQL注入,这一局两者打平。以上就是这次的分享,溜了溜了~~
本文使用 mdnice 排版