关于 Mybatis的 $ 和 # , 你真的知道他们的细节吗 ?

前言

在JDBC中,主要使用的是两种语句,一种是支持参数化和预编译的PrepareStatement,能够支持原生的Sql,也支持设置占位符的方式,参数化输入的参数,防止Sql注入,一种是支持原生Sql的Statement,有Sql注入的风险。

在使用Mybatis进行开发过程中,隐藏了底层具体使用哪一种语句的细节,我们通过使用#和$告诉Mybatis,我们实际上进行的是怎么样的操作,需要对语句进行参数化还是说直接保持原生状态就好。

今天我们主要看一下使用两种符号使用时系统应对Sql注入的表现和Mybatis在内部是如何对他们处理的源码分析。

#和$在应对Sql注入上的区别表现

利用现有应用程序,将(恶意的)SQL命令注入到后台数据库引擎执行的能力,它可以通过在Web表单中输入(恶意)SQL语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行SQL语句。

比如说根据学生姓名查学生信息,会传入一个name的参数,假设学生姓名是方方,那么Sql就是

SELECT id,name,age FROM student WHERE name = '方方';

在没有做防Sql注入的时候,我们的Sql语句可能是这么写的

SELECT id,name,age FROM student WHERE name = '${value}'

正常情况下查出姓名符合方方的学生信息。

但如果我们对传入的姓名参数做一些更改,比如改成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,所以等于对这个库执行了查所有的操作。

防范Sql注入的话,就是要把整个anything’ OR ‘x’='x中的单引号作为参数的一部分,而不是和Sql中的单引号进行拼接

使用了#即可在Mybatis中对参数进行转义

SELECT id,name,age FROM student WHERE name = #{name}

我们看一下发送到数据库端的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注入的风险。

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

推荐阅读更多精彩内容