MyBaits中#{}和${}的真正區(qū)別,${}的使用場(chǎng)景,#{}如何防止注入?
上一篇:最近一些想法
一、MyBatis中${}和#{}的區(qū)別
1.1?${}和#{}演示
數(shù)據(jù)庫(kù)數(shù)據(jù):

dao接口:
List?findByUsername(String?username) ;
List?findByUsername2(String?username) ;
Mapper.xml:
<select?id="findByUsername"?parameterType="java.lang.String"?resultType="com.lscl.entity.User">
????select?*?from?user?where?username?like?#{username}
select>
<select?id="findByUsername2"?parameterType="java.lang.String"?resultType="com.lscl.entity.User">
????select?*?from?user?where?username?like?'%${value}%'
select>
執(zhí)行測(cè)試代碼:
@Test
public?void?findByUsername()?throws?Exception?{
????InputStream?in?=?Resources.getResourceAsStream("SqlMapConfig.xml");
????SqlSessionFactoryBuilder?builder?=?new?SqlSessionFactoryBuilder();
????SqlSessionFactory?factory?=?builder.build(in);
????//?true:自動(dòng)提交
????SqlSession?session?=?factory.openSession(true);
????UserDao?userDao?=?session.getMapper(UserDao.class);
????List?userList?=?userDao.findByUsername("%小%");
????List?userList2?=?userDao.findByUsername2("小");
????System.out.println("userList:?");
????for?(User?user?:?userList)?{
????????System.out.println(user);
????}
????System.out.println("userList2:?");
????for?(User?user?:?userList2)?{
????????System.out.println(user);
????}
????session.close();
????in.close();
}
查看執(zhí)行結(jié)果:

發(fā)現(xiàn)都能夠查詢出來
1.2 SQL注入問題
${}會(huì)產(chǎn)生SQL注入,#{}不會(huì)產(chǎn)生SQL注入問題
我們做一個(gè)測(cè)試:
List?userList2?=?userDao.findByUsername2("?aaa'?or?1=1?--?");
System.out.println("userList2:?");
for?(User?user?:?userList2)?{
????System.out.println(user);
}
查詢生成的SQL語(yǔ)句:

我們傳遞的參數(shù)是aaa' or 1=1 --,導(dǎo)致查詢出來了全部的數(shù)據(jù)。
大家可以想象一下,如果我是要根據(jù)id刪除呢?
delete?from?user?where?id='${value}'
如果我傳遞的是:1' or 1=1; --,結(jié)果會(huì)是什么樣,我想大家應(yīng)該已經(jīng)知道了。
我這里id是Integer類型,不好測(cè)試,就不帶大家測(cè)試了,大家有興趣可以自己私下測(cè)試。
如果上面使用的是#{}就不會(huì)出現(xiàn)SQL注入的問題了

1.3?${}和#{}的區(qū)別
#{}匹配的是一個(gè)占位符,相當(dāng)于JDBC中的一個(gè)?,會(huì)對(duì)一些敏感的字符進(jìn)行過濾,編譯過后會(huì)對(duì)傳遞的值加上雙引號(hào),因此可以防止SQL注入問題。${}匹配的是真實(shí)傳遞的值,傳遞過后,會(huì)與sql語(yǔ)句進(jìn)行字符串拼接。${}會(huì)與其他sql進(jìn)行字符串拼接,不能預(yù)防sql注入問題。#{}和${}生成的SQL語(yǔ)句:
String?abc=“123”;
#{abc}="123"
${value}=123;
1.4?#{}底層是如何防止SQL注入的?
1.4.1 網(wǎng)上的答案
網(wǎng)上關(guān)于這類問題非常多,總結(jié)出來就兩個(gè)原因:
1)#{}底層采用的是PreparedStatement,會(huì)預(yù)編譯,因此不會(huì)產(chǎn)生SQL注入問題;
其實(shí)預(yù)編譯是MySQL自己本身的功能,和PreparedStatement沒關(guān)系;而且預(yù)編譯也不是咱們理解的那個(gè)預(yù)編譯,再者PreparedStatement底層默認(rèn)根本沒有用到預(yù)編譯(要我們手動(dòng)開啟)!詳細(xì)往下看
2)#{}不會(huì)產(chǎn)生字符串拼接,${}會(huì)產(chǎn)生字符串拼接,因此${}會(huì)出現(xiàn)SQL注入問題;
這兩個(gè)答案都經(jīng)不起深究,最終答案也只是停留在表面,也沒人知道具體是為什么。
1.4.2 為什么能防止SQL注入?
我們翻開MySQL驅(qū)動(dòng)的源碼一看究竟;
打開PreparedStatement類的setString()方法(MyBatis在#{}傳遞參數(shù)時(shí),是借助setString()方法來完成,${}則不是):

setString()方法全部源碼:
另外,搜索公眾號(hào)互聯(lián)網(wǎng)架構(gòu)師后臺(tái)回復(fù)“2T”,獲取一份驚喜禮包。
public?void?setString(int?parameterIndex,?String?x)?throws?SQLException?{
????????synchronized(this.checkClosed().getConnectionMutex())?{
????????????if?(x?==?null)?{
????????????????this.setNull(parameterIndex,?1);
????????????}?else?{
????????????????this.checkClosed();
????????????????int?stringLength?=?x.length();
????????????????StringBuilder?buf;
????????????????if?(this.connection.isNoBackslashEscapesSet())?{
????????????????????boolean?needsHexEscape?=?this.isEscapeNeededForString(x,?stringLength);
????????????????????Object?parameterAsBytes;
????????????????????byte[]?parameterAsBytes;
????????????????????if?(!needsHexEscape)?{
????????????????????????parameterAsBytes?=?null;
????????????????????????buf?=?new?StringBuilder(x.length()?+?2);
????????????????????????buf.append('\'');
????????????????????????buf.append(x);
????????????????????????buf.append('\'');
????????????????????????if?(!this.isLoadDataQuery)?{
????????????????????????????parameterAsBytes?=?StringUtils.getBytes(buf.toString(),?this.charConverter,?this.charEncoding,?this.connection.getServerCharset(),?this.connection.parserKnowsUnicode(),?this.getExceptionInterceptor());
????????????????????????}?else?{
????????????????????????????parameterAsBytes?=?StringUtils.getBytes(buf.toString());
????????????????????????}
????????????????????????this.setInternal(parameterIndex,?parameterAsBytes);
????????????????????}?else?{
????????????????????????parameterAsBytes?=?null;
????????????????????????if?(!this.isLoadDataQuery)?{
????????????????????????????parameterAsBytes?=?StringUtils.getBytes(x,?this.charConverter,?this.charEncoding,?this.connection.getServerCharset(),?this.connection.parserKnowsUnicode(),?this.getExceptionInterceptor());
????????????????????????}?else?{
????????????????????????????parameterAsBytes?=?StringUtils.getBytes(x);
????????????????????????}
????????????????????????this.setBytes(parameterIndex,?parameterAsBytes);
????????????????????}
????????????????????return;
????????????????}
????????????????String?parameterAsString?=?x;
????????????????boolean?needsQuoted?=?true;
????????????????if?(this.isLoadDataQuery?||?this.isEscapeNeededForString(x,?stringLength))?{
????????????????????needsQuoted?=?false;
????????????????????buf?=?new?StringBuilder((int)((double)x.length()?*?1.1D));
????????????????????buf.append('\'');
????????????????????for(int?i?=?0;?i?//遍歷字符串,獲取到每個(gè)字符
????????????????????????char?c?=?x.charAt(i);
????????????????????????switch(c)?{
????????????????????????case?'\u0000':
????????????????????????????buf.append('\\');
????????????????????????????buf.append('0');
????????????????????????????break;
????????????????????????case?'\n':
????????????????????????????buf.append('\\');
????????????????????????????buf.append('n');
????????????????????????????break;
????????????????????????case?'\r':
????????????????????????????buf.append('\\');
????????????????????????????buf.append('r');
????????????????????????????break;
????????????????????????case?'\u001a':
????????????????????????????buf.append('\\');
????????????????????????????buf.append('Z');
????????????????????????????break;
????????????????????????case?'"':
????????????????????????????if?(this.usingAnsiMode)?{
????????????????????????????????buf.append('\\');
????????????????????????????}
????????????????????????????buf.append('"');
????????????????????????????break;
????????????????????????case?'\'':
????????????????????????????buf.append('\\');
????????????????????????????buf.append('\'');
????????????????????????????break;
????????????????????????case?'\\':
????????????????????????????buf.append('\\');
????????????????????????????buf.append('\\');
????????????????????????????break;
????????????????????????case?'¥':
????????????????????????case?'?':
????????????????????????????if?(this.charsetEncoder?!=?null)?{
????????????????????????????????CharBuffer?cbuf?=?CharBuffer.allocate(1);
????????????????????????????????ByteBuffer?bbuf?=?ByteBuffer.allocate(1);
????????????????????????????????cbuf.put(c);
????????????????????????????????cbuf.position(0);
????????????????????????????????this.charsetEncoder.encode(cbuf,?bbuf,?true);
????????????????????????????????if?(bbuf.get(0)?==?92)?{
????????????????????????????????????buf.append('\\');
????????????????????????????????}
????????????????????????????}
????????????????????????????buf.append(c);
????????????????????????????break;
????????????????????????default:
????????????????????????????buf.append(c);
????????????????????????}
????????????????????}
????????????????????buf.append('\'');
????????????????????parameterAsString?=?buf.toString();
????????????????}
????????????????buf?=?null;
????????????????byte[]?parameterAsBytes;
????????????????if?(!this.isLoadDataQuery)?{
????????????????????if?(needsQuoted)?{
????????????????????????parameterAsBytes?=?StringUtils.getBytesWrapped(parameterAsString,?'\'',?'\'',?this.charConverter,?this.charEncoding,?this.connection.getServerCharset(),?this.connection.parserKnowsUnicode(),?this.getExceptionInterceptor());
????????????????????}?else?{
????????????????????????parameterAsBytes?=?StringUtils.getBytes(parameterAsString,?this.charConverter,?this.charEncoding,?this.connection.getServerCharset(),?this.connection.parserKnowsUnicode(),?this.getExceptionInterceptor());
????????????????????}
????????????????}?else?{
????????????????????parameterAsBytes?=?StringUtils.getBytes(parameterAsString);
????????????????}
????????????????this.setInternal(parameterIndex,?parameterAsBytes);
????????????????this.parameterTypes[parameterIndex?-?1?+?this.getParameterIndexOffset()]?=?12;
????????????}
????????}
????}
我們執(zhí)行#{}的查詢語(yǔ)句,打斷點(diǎn)觀察:

最終傳遞的參數(shù)如下:

最終傳遞的參數(shù)為:'aaa\' or 1=1 --
咱們?cè)跀?shù)據(jù)庫(kù)中執(zhí)行如下SQL語(yǔ)句(肯定是查詢不到數(shù)據(jù)的):
select?*?from?user?where?username?like?'aaa\'?or?1=1?--?'

如果把PreparedStatement加的那根"/"去掉呢?我們執(zhí)行SQL試試:
select?*?from?user?where?username?like?'aaa'?or?1=1?--?'

我們也可以通過MySQL的日志來觀察#{}和${}產(chǎn)生的SQL語(yǔ)句來分析問題:
1)開啟MySQL日志:
在MySQL配置文件中的[mysqld]下增加如下配置:
#?是否開啟mysql日志??0:關(guān)閉(默認(rèn)值)?1:開啟
general-log=1
#?mysql?日志的存放位置
general_log_file="D:/query.log"

2)重啟MySQL服務(wù)(要以管理員身份運(yùn)行):

net?stop?mysql
net?start?mysql
使用mybatis分別執(zhí)行如下兩條SQL語(yǔ)句:

查看MySQL日志:

1.5?#{}和${}的應(yīng)用場(chǎng)景
既然#{}比${}好那么多,那為什么還要有${}這個(gè)東西存在呢?干脆都用#{}不就萬(wàn)事大吉嗎?關(guān)注互聯(lián)網(wǎng)架構(gòu)師
其實(shí)不是的,${}也有用武之地,我們都知道${}會(huì)產(chǎn)生字符串拼接,來生成一個(gè)新的字符串
1.5.1 ${}和#{}用法上的區(qū)別
例如現(xiàn)在要進(jìn)行模糊查詢,查詢user表中姓張的所有員工的信息
另外,搜索公眾號(hào)互聯(lián)網(wǎng)架構(gòu)師后臺(tái)回復(fù)“2T”,獲取一份驚喜禮包。
sql語(yǔ)句為:select * from user where name like '張%'
此時(shí)如果傳入的參數(shù)是 “張”
如果使用${}:select * from user where name like '${value}%'
生成的sql語(yǔ)句:select * from user where name like '張%'
如果使用#{}:select * from user where name like #{value}"%"
生成的sql語(yǔ)句:select * from user where name like '張'"%"
如果傳入的參數(shù)是 “張%”
使用#{}:select * from user where name like #{value}
生成的sql語(yǔ)句:select * from user where name like '張%'
使用${}:select * from user where name like '${value}'
生成的sql語(yǔ)句:select * from user where name like '張%'
通過上面的SQL語(yǔ)句我們能夠發(fā)現(xiàn)#{}是會(huì)加上雙引號(hào),而${}匹配的是真實(shí)的值。
還有一點(diǎn)就是如果使用${}的話,里面必須要填value,即:${value},#{}則隨意
1.5.2 什么情況下用${}?
場(chǎng)景舉例:
代碼測(cè)試:
執(zhí)行之后,發(fā)現(xiàn)執(zhí)行成功
我們可以切換一下,把${}改成#{},會(huì)出現(xiàn)SQL語(yǔ)法錯(cuò)誤的異常

1.6 總結(jié)
1.6.1 SQL注入問題
#{}之所以能夠預(yù)防SQL注入是因?yàn)榈讓邮褂昧薖reparedStatement類的setString()方法來設(shè)置參數(shù),此方法會(huì)獲取傳遞進(jìn)來的參數(shù)的每個(gè)字符,然后進(jìn)行循環(huán)對(duì)比,如果發(fā)現(xiàn)有敏感字符(如:?jiǎn)我?hào)、雙引號(hào)等),則會(huì)在前面加上一個(gè)'/'代表轉(zhuǎn)義此符號(hào),讓其變?yōu)橐粋€(gè)普通的字符串,不參與SQL語(yǔ)句的生成,達(dá)到防止SQL注入的效果。${}本身設(shè)計(jì)的初衷就是為了參與SQL語(yǔ)句的語(yǔ)法生成,自然而然會(huì)導(dǎo)致SQL注入的問題(不會(huì)考慮字符過濾問題)。#{}和${}用法總結(jié) 1)#{}在使用時(shí),會(huì)根據(jù)傳遞進(jìn)來的值來選擇是否加上雙引號(hào),因此我們傳遞參數(shù)的時(shí)候一般都是直接傳遞,不用加雙引號(hào),${}則不會(huì),我們需要手動(dòng)加#{}中可以寫任意的值,${}則必須使用value;即:${value}#{}針對(duì)SQL注入進(jìn)行了字符過濾,${}則只是作為普通傳值,并沒有考慮到這些問題4)#{}的應(yīng)用場(chǎng)景是為給SQL語(yǔ)句的where字句傳遞條件值,${}的應(yīng)用場(chǎng)景是為了傳遞一些需要參與SQL語(yǔ)句語(yǔ)法生成的值。
全棧架構(gòu)社區(qū)交流群
?「全棧架構(gòu)社區(qū)」建立了讀者架構(gòu)師交流群,大家可以添加小編微信進(jìn)行加群。歡迎有想法、樂于分享的朋友們一起交流學(xué)習(xí)。
看完本文有收獲?請(qǐng)轉(zhuǎn)發(fā)分享給更多人
往期資源:
