在JDBC中,主要使用的是两种语句,一种是支持参数化和预编译的PrepareStatement,可以支持原生的Sql,也支持设置占位符的方式,参数化输入的参数,防止Sql注入,一种是支持原生Sql的Statement,有Sql注入的风险。sql
在使用Mybatis进行开发过程当中,隐藏了底层具体使用哪种语句的细节,咱们经过使用#和$告诉Mybatis,咱们实际上进行的是怎么样的操做,须要对语句进行参数化仍是说直接保持原生状态就好。数据库
今天咱们主要看一下使用两种符号使用时系统应对Sql注入的表现和Mybatis在内部是如何对他们处理的源码分析。安全
利用现有应用程序,将(恶意的)SQL命令注入到后台数据库引擎执行的能力,它能够经过在Web表单中输入(恶意)SQL语句获得一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行SQL语句。session
好比说根据学生姓名查学生信息,会传入一个name的参数,假设学生姓名是方方,那么Sql就是app
SELECT id,name,age FROM student WHERE name = '方方';
在没有作防Sql注入的时候,咱们的Sql语句多是这么写的源码分析
<select id="fetchStudentByName" parameterType="String" resultType="entity.StudentEntity"> SELECT id,name,age FROM student WHERE name = '${value}' </select>
正常状况下查出姓名符合方方的学生信息。fetch
但若是咱们对传入的姓名参数作一些更改,好比改为anything' OR 'x'='x,那么拼接而成的Sql就变成了网站
SELECT id,name,age FROM student WHERE name = 'anything' OR 'x'='x'
库里面全部的学生信息都被拉了出来,是否是很可怕。缘由就是传入的anything' OR 'x'='x和原有的单引号,正好组成了 'anything' OR 'x'='x',而OR后面恒等于1,因此等于对这个库执行了查全部的操做。ui
防范Sql注入的话,就是要把整个anything' OR 'x'='x中的单引号做为参数的一部分,而不是和Sql中的单引号进行拼接spa
使用了#便可在Mybatis中对参数进行转义
<select id="fetchStudentByName" parameterType="String" resultType="entity.StudentEntity"> SELECT id,name,age FROM student WHERE name = #{name} </select>
咱们看一下发送到数据库端的Sql语句长什么样子。
SELECT id,name,age FROM student WHERE name = 'anything\' OR \'x\'=\'x'
从上述代码中咱们能够看到参数中的全部单引号通通被转移了,这都是JDBC中PrepareStatement的功劳,若是在数据库服务端开启了预编译,则是服务端来作了这件事情。
具体能够看我以前写的这篇: JDBC与Mysql的那些事,里面解释了为什么PrepareStatement能作到这件事情。
在之前的文章中,咱们说明过Mybatis的执行流程主要部件,SqlSession 提供给用户操做的Api,Executor 具体执行对数据库的操做,但其实在Executor内部还会再委托给StatementHandler这个接口。
这个Handler的实现类就是表明了JDBC中的操做语句,CallableStatementHandler、PrepareStatementHandler和SimpleStatementHandler就会表明对JDBC中的CallableStatement,PrepareStatement和Statement,这些handler的内部就会调用JDBC中的相关Statement。
类比Mybatis的执行流程和JDBC原有的咱们使用的方法就是。
Mybatis: Sqlsession -> Executor -> StatementHandler -> ResultHandler
JDBC: Connection -> Statement -> Result
所以咱们能够知道对JDBC语句的操做都会在StatementHandler内部。
在PrepareStatementHandler中会使用paramterize对Statement进行参数化,在其中他会委托给DefualtParameterHandler进行操做。咱们经过两种不一样的语句,看一下,Debug下这段代码的不一样。
首先是使用$符号,它是会直接在Sql中进行拼接的,从下图可知,在进行参数化的时候,Sql语句已经被拼接完成了,见originSql。
进入DefualtParameterHandler内部,以下图可知,咱们看到,这儿boundSql的ParameterMappings不存在,因此不用执行第二个红框处,设置对应占位符的操做。
而后,咱们看一下当使用#的时候,一样的代码,会获得什么样的处理结果。从下图可知,当使用#的时候,原有的#{value}被替换成了?号,也就是咱们熟知的JDBC中的占位符。
再进入DefualtParameterHandler的时候, 此时会有ParameterMappings,value -> anything' OR 'x'='x',找到合适的TypeHandler塞入PrepareStatement中。
**从上文的分析中,咱们获得的就是,当使用的时候,的时候,{value},是直接被替换为了对应的值,没有参数映射,不会进行设置占位符的操做,当使用#的时候,#{}会被替换为?号,有参数映射,会在DefaultParameterHandler中进行设置占位符的操做。
问题
1 为何默认使用的语句是PrepareStatementHandler
2 和#是何时被替换的,为何对应的BoundSql,$时没有映射,#有映射。
带着这两个问题咱们来看一下,Mybatis的初始化阶段,为节省篇幅,仅列出大体路径,和关键代码。
Mybatis是经过SqlSessionFactory build出来的,会解析映射文件,大体路径就是
SqlSessionFactoryBuilder -> XmlConfigBuilder->XMLMapperBuilder->XMLStatementBuilder。
在XMLStatementBuilder的parseStatementNode负责了生成MappedStatement,首先回答第一个问题。当你不指定statementType时,Mybatis默认使用的就是PrepareStatementHandler,这里的StatementType,在后续流程中使用RoutingStatementHandler选择使用哪个StatementHandler。
而后继续看第二个问题,$和#是怎么被替换的。
在以前咱们提到了,BoundSql中包含了Sql主体,同时其中的参数映射决定了后续是否要进行参数化,在$和#时,表现是不一样的。
BoudSql来自于MappedStatement,在MappedStatement中,获取BoundSql的任务会委托给SqlSource接口。因此咱们接下来主要看SqlSource是如何生成的。
XMLLandDriver能够理解为就是用来解析Mybatis定制的XML符号的语句。他会把具体解析符号的职责交给XMLScriptBuilder的parseScriptNode方法。
parseDynamicTags中会把语句用TextSql包装起来,而后使用isDynamic方法,在方法中使用GerenericTokenParser判断是不是动态语句。若是其中包含$,就是动态的,若是是#就不是动态的,使用的Handler是DrynamicCheckerTokenParser。
在进入parse方法后,主要看如下这一段。
这里会使用TokenHandler不一样的实现类,对表达式进行进一步的处理,这里是对Sql自后的完善,在判断isDynamic中,使用的是DrynamicCheckerTokenParser,一个最简单的实现。
parse完成后,若是isDynamic是true的话,就是动态语句,使用DynamicSqlSource。
若是是非动态的话,其实通常就是指使用了#的语句,使用RawSqlSource,在其中,还会进一步解析。
从下图中能够看到,这个TokenParser这回使用的是#{},并且使用的是ParameterMappingTokenHandler。
ParameterMappingTokenHandler的handlerToken方法中,完成了添加参数映射和替换#{value}为?的职责。
从以上咱们能够知道,使用#在初始化阶段,会被替换成?号,同时生成参数映射,而使用$在初始化阶段,没有什么特别的地方,仅仅作了一个是否动态语句的判断。
在初始化完毕后,咱们进入getBoundSql方法,看一下DynamicSqlSource和StaticSource在此刻作了什么,首先是DynamicSqlSource。
在其中,首先会生成一个DynamicContext,主要就是 生成bindings,一个是 "_parameter" -> "anything' OR 'x'='x",一个是"_databaseId" -> "null"
而后使用了apply方法,我理解这里是要去作替换了。具体仍是使用${}去判断,和上文一致,只不过这里使用的是BindingTokenParser。
看一下BindingTokenParser的HandleToken方法。
上述代码的效果,就是会使用Ognl,使用value在Bindings中,找对应的值,最后返回,拼接在Sql中,这也就是为何会有Sql注入风险的缘由。使用value是由于Ognl去找的时候,就会使用value这个默认值,因此须要在bindings额外加入这么一个键值对,有兴趣能够继续往下看ONGL相关的东西。
接下来是生成SqlSource,使用的是SqlSourceBuilder的parse方法。
在前文介绍过,在这个parse方法里,是用#{}来判断的,因此走不到ParameterMappingTokenHandler的handlerToken方法,也就没法添加参数映射了,这个直接返回一个StaticSqlSource,这也解释了为何使用$时,参数映射为空。
再接下去就是获取BoundSql,使用的是StaticSqlSource,直接根据参数,实例化了一个,参数映射为空。
当使用#的时候,使用的就是StaticSqlSource,直接实例化,由于参数映射在以前初始化的阶段,也生成好了,因此很简单的一个流程。
后续的流程,就和Mybatis正常的流程一致了。
本文主要剖析了Mybatis中$和#两种符号使用上的不一样,以及使用这两种符号时,源码流程上的区别。建议你们都使用#号,在orm这层也规避到Sql注入的风险。
假若您有疑问或者有进一步想了解内容,欢迎留言给我。