在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注入的風險。
假若您有疑問或者有進一步想了解內容,歡迎留言給我。