一嘉蕾、MyBatis中${}和#{}的區(qū)別
1.1 ${}和#{}演示
數(shù)據(jù)庫(kù)數(shù)據(jù):
dao接口:
List<User> findByUsername(String username);
List<User> 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>
<!-- 使用${},注意${}中的值必須要填value -->
<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<User> userList = userDao.findByUsername("%小%");
List<User> 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)都能夠查詢出來(lái)
1.2 SQL注入問(wèn)題
${}
會(huì)產(chǎn)生SQL注入,#{}
不會(huì)產(chǎn)生SQL注入問(wèn)題
我們做一個(gè)測(cè)試:
List<User> 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)致查詢出來(lái)了全部的數(shù)據(jù)。大家可以想象一下,如果我是要根據(jù)id刪除呢?
delete from user where id='${value}'
如果我傳遞的是:1' or 1=1; --蕾久,結(jié)果會(huì)是什么樣,我想大家應(yīng)該已經(jīng)知道了拌夏。如果上面使用的是#{}就不會(huì)出現(xiàn)SQL注入的問(wèn)題了
1.3 ${}和#{}的區(qū)別
#{}
匹配的是一個(gè)占位符僧著,相當(dāng)于JDBC中的一個(gè)?履因,會(huì)對(duì)一些敏感的字符進(jìn)行過(guò)濾,編譯過(guò)后會(huì)對(duì)傳遞的值加上雙引號(hào)盹愚,因此可以防止SQL注入問(wèn)題栅迄。
${}
匹配的是真實(shí)傳遞的值,傳遞過(guò)后皆怕,會(huì)與sql語(yǔ)句進(jìn)行字符串拼接毅舆。${}
會(huì)與其他sql進(jìn)行字符串拼接,不能預(yù)防sql注入問(wèn)題愈腾。
查看#{}和${}生成的SQL語(yǔ)句
String abc=“123”;
#{abc}="123"
${value}=123;
1.4 為什么能防止SQL注入憋活?
我們翻開MySQL驅(qū)動(dòng)的源碼一看究竟;打開PreparedStatement
類的setString()方法(MyBatis在#{}
傳遞參數(shù)時(shí)虱黄,是借助setString()
方法來(lái)完成悦即,${}
則不是):
setString()方法全部源碼:
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 < stringLength; ++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 -- '
1.5 #{}和${}的應(yīng)用場(chǎng)景
既然#{}
比${}
好那么多,那為什么還要有${}
這個(gè)東西存在呢泳叠?干脆都用#{}
不就萬(wàn)事大吉嗎作瞄?
其實(shí)不是的,${}
也有用武之地析二,我們都知道${}
會(huì)產(chǎn)生字符串拼接粉洼,來(lái)生成一個(gè)新的字符串
1.5.1 ${}和#{}用法上的區(qū)別
例如現(xiàn)在要進(jìn)行模糊查詢节预,查詢user表中姓張的所有員工的信息
sql語(yǔ)句為:select * from user where name like '張%'
1叶摄、此時(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 '張'"%"
2、如果傳入的參數(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 '張%'
通過(guò)上面的SQL語(yǔ)句我們能夠發(fā)現(xiàn)#{}是會(huì)加上雙引號(hào)安拟,而${}匹配的是真實(shí)的值蛤吓。
1.5.2 什么情況下用${}?
場(chǎng)景舉例:
代碼測(cè)試:
執(zhí)行之后糠赦,發(fā)現(xiàn)執(zhí)行成功
我們可以切換一下会傲,把${}改成#{},會(huì)出現(xiàn)SQL語(yǔ)法錯(cuò)誤的異常
二拙泽、總結(jié)
MyBatis的#{}
之所以能夠預(yù)防SQL注入是因?yàn)榈讓邮褂昧?code>PreparedStatement類的setString()
方法來(lái)設(shè)置參數(shù)淌山,此方法會(huì)獲取傳遞進(jìn)來(lái)的參數(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注入的問(wèn)題(不會(huì)考慮字符過(guò)濾問(wèn)題)个粱。
#{}
在使用時(shí),會(huì)根據(jù)傳遞進(jìn)來(lái)的值來(lái)選擇是否加上雙引號(hào)翻翩,因此我們傳遞參數(shù)的時(shí)候一般都是直接傳遞都许,不用加雙引號(hào),${}
則不會(huì)体斩,我們需要手動(dòng)加
#{}
針對(duì)SQL注入進(jìn)行了字符過(guò)濾梭稚,${}
則只是作為普通傳值,并沒有考慮到這些問(wèn)題
#{}
的應(yīng)用場(chǎng)景是為給SQL語(yǔ)句的where字句傳遞條件值絮吵,${}
的應(yīng)用場(chǎng)景是為了傳遞一些需要參與SQL語(yǔ)句語(yǔ)法生成的值弧烤。
三、項(xiàng)目中使用like
MyBaits中用#{}防止注入
方式一
<if test="params.searchKey != null and params.searchKey != ''">
and client_id like concat('%', #{params.searchKey}, '%')
</if>
方式二
1蹬敲、查詢條件對(duì)象
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("serial")
@ApiModel(value = "QueryLuckyActivityVO")
public class QueryLuckyActivityVO {
@ApiModelProperty(value = "抽獎(jiǎng)活動(dòng)ID")
private String activityNo;
@ApiModelProperty(value = "抽獎(jiǎng)活動(dòng)標(biāo)題")
private String activityName;
@ApiModelProperty(value = "參與人數(shù)(大于)")
private Integer joinNumberStart;
@ApiModelProperty(value = "參與人數(shù)(小于)")
private Integer joinNumberEnd;
@ApiModelProperty(value = "狀態(tài)")
private PublishStatusEnum publishStatus;
@ApiModelProperty(value = "活動(dòng)類型(1=抽獎(jiǎng)+兌獎(jiǎng)暇昂;2=僅抽獎(jiǎng);3=僅兌獎(jiǎng))")
private String activityType;
public String getActivityNo() {
if(StringUtil.isNotBlank(activityNo)){
return "%"+activityNo+"%";
}
return activityNo;
}
public String getActivityName() {
if(StringUtil.isNotBlank(activityName)){
return "%"+activityName+"%";
}
return activityName;
}
}
2伴嗡、mybatis
<select id="findPage" resultType="com.alanchen.lucky.dto.LuckyActivityDTO">
select * from t_lucky_activity m
<where>
m.deleted = 0
<if test="vo.activityNo != null and vo.activityNo!=''">
and m.activity_no like #{vo.activityNo}
</if>
<if test="vo.activityName != null and vo.activityName!=''">
and m.activity_name like #{vo.activityName}
</if>
<if test="vo.joinNumberStart != null">
and m.join_number <![CDATA[ >= ]]> #{vo.joinNumberStart}
</if>
</where>
order by m.create_time desc
</select>