共计 9558 个字符,预计需要花费 24 分钟才能阅读完成。
一、MyBatis 中 ${}和 #{}的区别
1.1 ${}
和 #{}
演示
数据库数据:
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>
执行测试代码:
@Test
public void findByUsername() throws Exception {InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
// true: 主动提交
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();}
查看执行后果:
发现都可能查问进去
1.2 SQL 注入问题
${}
会产生 SQL 注入,#{}
不会产生 SQL 注入问题
咱们做一个测试:
List<User> userList2 = userDao.findByUsername2("aaa' or 1=1 -- ");
System.out.println("userList2:");
for (User user : userList2) {System.out.println(user);
}
查问生成的 SQL 语句:
咱们传递的参数是 aaa’ or 1=1 –,导致查问进去了全副的数据。
大家能够设想一下,如果我是要依据 id 删除呢?
delete from user where id='${value}'
如果我传递的是:1′ or 1=1; –,后果会是什么样,我想大家应该曾经晓得了。
我这里 id 是 Integer 类型,不好测试,就不带大家测试了,大家有趣味能够本人私下测试。
如果下面应用的是 #{}
就不会呈现 SQL 注入的问题了
1.3 ${}
和 #{}
的区别
#{}
匹配的是一个占位符,相当于 JDBC 中的一个?,会对一些敏感的字符进行过滤,编译过后会对传递的值加上双引号,因而能够避免 SQL 注入问题。
${}
匹配的是实在传递的值,传递过后,会与 sql 语句进行字符串拼接。${}会与其余 sql 进行字符串拼接,不能预防 sql 注入问题。
查看 #{}
和${}
生成的 SQL 语句:
String abc=“123”;
#{abc}="123"
${value}=123;
1.4 #{}
底层是如何避免 SQL 注入的?
1.4.1 网上的答案
网上对于这类问题十分多,总结进去就两个起因:
1)#{}
底层采纳的是 PreparedStatement,会预编译,因而不会产生 SQL 注入问题;
其实预编译是 MySQL 本人自身的性能,和 PreparedStatement 没关系;而且预编译也不是咱们了解的那个预编译,再者 PreparedStatement 底层默认基本没有用到预编译(要咱们手动开启)!具体往下看
2)#{}
不会产生字符串拼接,${}
会产生字符串拼接,因而 ${}
会呈现 SQL 注入问题;
这两个答案都经不起深究,最终答案也只是停留在外表,也没人晓得具体是为什么。
1.4.2 为什么能避免 SQL 注入?
咱们打开 MySQL 驱动的源码一看到底;
关上 PreparedStatement 类的 setString()办法(MyBatis 在 #{}
传递参数时,是借助 setString()办法来实现,${}
则不是):
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) { // 遍历字符串,获取到每个字符
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;
}
}
}
咱们执行 #{}
的查问语句,打断点察看:
最终传递的参数如下:
最终传递的参数为:'aaa\' or 1=1 --
咱们在数据库中执行如下 SQL 语句(必定是查问不到数据的):
select * from user where username like 'aaa\' or 1=1 -- '
如果把 PreparedStatement 加的那根 ”/” 去掉呢?咱们执行 SQL 试试:
select * from user where username like 'aaa' or 1=1 -- '
咱们也能够通过 MySQL 的日志来察看 #{}
和${}
产生的 SQL 语句来剖析问题:
1)开启 MySQL 日志:
在 MySQL 配置文件中的 [mysqld] 下减少如下配置:
# 是否开启 mysql 日志 0: 敞开(默认值) 1: 开启
general-log=1
# mysql 日志的寄存地位
general_log_file="D:/query.log"
2)重启 MySQL 服务(要以管理员身份运行):
net stop mysql
net start mysql
应用 mybatis 别离执行如下两条 SQL 语句:
查看 MySQL 日志:
1.5 #{}
和 ${}
的利用场景
既然 #{}
比${}
好那么多,那为什么还要有 ${}
这个货色存在呢?罗唆都用 #{}
不就高枕无忧吗?
其实不是的,${}
也有用武之地,咱们都晓得 ${}
会产生字符串拼接,来生成一个新的字符串
1.5.1 ${}和 #{}用法上的区别
例如当初要进行含糊查问,查问 user 表中姓张的所有员工的信息
sql 语句为:select * from user where name like '张 %'
此时如果传入的参数是“张”
如果应用${}
:select * from user where name like '${value}%'
生成的 sql 语句:select * from user where name like '张 %'
如果应用#{}
:select * from user where name like #{value}"%"
生成的 sql 语句:select * from user where name like '张'"%"
如果传入的参数是“张 %”
应用#{}
:select * from user where name like #{value}
生成的 sql 语句:select * from user where name like '张 %'
应用${}
:select * from user where name like '${value}'
生成的 sql 语句:select * from user where name like '张 %'
通过下面的 SQL 语句咱们可能发现 #{}
是会加上双引号,而 ${}
匹配的是实在的值。
还有一点就是如果应用 ${}
的话,外面必须要填 value,即:${value}
,#{}
则随便
1.5.2 什么状况下用${}
?
场景举例:
代码测试:
执行之后,发现执行胜利
咱们能够切换一下,把 ${}
改成#{}
,会呈现 SQL 语法错误的异样
1.6 总结
1.6.1 SQL 注入问题
MyBatis 的 #{}
之所以可能预防 SQL 注入是因为底层应用了 PreparedStatement 类的 setString()办法来设置参数,此办法会获取传递进来的参数的每个字符,而后进行循环比照,如果发现有敏感字符(如:单引号、双引号等),则会在后面加上一个 ’/’ 代表本义此符号,让其变为一个一般的字符串,不参加 SQL 语句的生成,达到避免 SQL 注入的成果。
其次 ${}
自身设计的初衷就是为了参加 SQL 语句的语法生成,自然而然会导致 SQL 注入的问题(不会思考字符过滤问题)。
1.6.2 #{}
和 ${}
用法总结 1)#{}
在应用时,会依据传递进来的值来抉择是否加上双引号,因而咱们传递参数的时候个别都是间接传递,不必加双引号,${}
则不会,咱们须要手动加
2)在传递一个参数时,咱们说了 #{}
中能够写任意的值,${}
则必须应用 value;即:${value}
3)#{}
针对 SQL 注入进行了字符过滤,${}
则只是作为一般传值,并没有思考到这些问题
4)#{}
的利用场景是为给 SQL 语句的 where 字句传递条件值,${}
的利用场景是为了传递一些须要参加 SQL 语句语法生成的值。