4千字帶你看懂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="fetchStudentByName" parameterType="String" resultType="entity.StudentEntity"> SELECT id,name,age FROM student WHERE name = '${value}' </select> 

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

但如果我們對傳入的姓名參數做一些更改,比如改成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="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注入的風險。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章