【Mybatis系列】從源碼角度理解Mybatis的$和#的做用

前言

在JDBC中,主要使用的是兩種語句,一種是支持參數化和預編譯的PrepareStatement,可以支持原生的Sql,也支持設置佔位符的方式,參數化輸入的參數,防止Sql注入,一種是支持原生Sql的Statement,有Sql注入的風險。sql

在使用Mybatis進行開發過程當中,隱藏了底層具體使用哪種語句的細節,咱們經過使用#和$告訴Mybatis,咱們實際上進行的是怎麼樣的操做,須要對語句進行參數化仍是說直接保持原生狀態就好。數據庫

今天咱們主要看一下使用兩種符號使用時系統應對Sql注入的表現和Mybatis在內部是如何對他們處理的源碼分析。安全

和$在應對Sql注入上的區別表現

利用現有應用程序,將(惡意的)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注入的風險。

輸入圖片說明

假若您有疑問或者有進一步想了解內容,歡迎留言給我。

相關文章
相關標籤/搜索