引言
在使用 MyBatis 进行数据库操作时,#{} 和 ${} 是两个常用的占位符符号。它们虽然看起来相似,但在实际应用中有着本质的区别。
1. ${} —— 字符串拼接
${}
是 MyBatis 中的 字符串拼接占位符,用于直接将参数值拼接到 SQL 语句中。它会将传入的参数值直接替换到 SQL 中的 ${}
所在的位置,而不会进行任何的处理或转义。
1.1 工作原理
${}
会直接将参数值插入到 SQL 字符串中,不会使用 PreparedStatement 进行参数化传递。- 因为没有任何处理和转义,这种方式容易导致 SQL 注入风险。
1.2 示例
假设我们有一个 SQL 查询:
<select id="findUserByColumn" parameterType="String" resultType="User">
SELECT * FROM user WHERE user_name = ${userName}
</select>
在这个查询中, ${userName}
会直接被替换为传入的参数值。例如,如果传入的参数是 “admin”,最终的 SQL 查询会是:
SELECT * FROM user WHERE user_name = 'admin'
这里的 ${userName}
直接替换成了 admin,没有任何转义或检查。如果用户传入恶意的 SQL 语句片段(比如 “admin’ or 1=1 --”),name生成的SQL语句为:
SELECT * FROM user WHERE user_name = 'admin' or 1=1 --'
那么这样就会查出user表中所有的用户数据,出现了SQL注入的问题
1.3 使用场景
通常情况下,${}
适用于那些不需要用户输入的地方,如表名、列名等静态部分。在这种情况下,静态部分是由程序内部控制的,不会受到外部输入的影响。
2.#{} —— 参数占位符
#{}
是 MyBatis 中的 参数占位符,用于将参数传递到 SQL 语句中。它的作用是将参数值进行处理并填充到 SQL 语句中,在传递参数时会进行预处理,避免 SQL 注入风险。
2.1 工作原理
- MyBatis 会将
#{}
中的参数值自动进行转义,将 sql 中的#{}
替换为 ? 号。 - 它不会直接将参数值拼接到 SQL 语句中,而是通过
PreparedStatement
的方式将参数作为 SQL 的一部分传入。
2.2 示例
假设我们有一个 SQL 查询:
<select id="findUserByName" parameterType="String" resultType="User">
SELECT * FROM users WHERE user_name = #{username}
</select>
在这个查询中,#{username}
会被 MyBatis 替换为传入的参数值,并通过 PreparedStatement
安全地传递给数据库。比如,如果传入的参数是 “admin”,最终的 SQL 查询会是:
SELECT * FROM users WHERE username = 'admin'
MyBatis 会自动处理转义和类型转换,确保 username 参数的正确传递,避免 SQL 注入问题。
2.3 使用场景
#{}
适用于所有需要传递用户输入参数的地方,确保应用程序的安全性和稳定性。
3.#{ }和 ${ }的区别
-
#{}
匹配的是一个占位符,相当于JDBC中的一个?,会对一些敏感的字符进行过滤,编译过后会对传递的值加上双引号,因此可以防止SQL注入问题。 -
${}
匹配的是真实传递的值,传递过后,会与sql语句进行字符串拼接。${}
会与其他sql进行字符串拼接,不能预防sql注入问题。
4.#{}底层是如何防止SQL注入的?
MyBatis在使用#{}
传递参数时,是借助PreparedStatement
类的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;
}
}
}
执行#{}
的查询语句,打断点:
所以最终传递的参数为:
'admin\' or 1=1 --
所以在数据库执行的SQL语句为:
select * from user where user_name = 'admin\' or 1=1 -- '
显然查询不到user_name为admin\' or 1=1 --
的数据
但是如果SQL语句为:
select * from user where user_name = 'admin' or 1=1 -- '
因为有or 1=1
的存在,就会查询出所有数据,出现SQL注入问题
5.其他
假设有一条SQL语句:
select * from #{value}
此时传入一个数据库表名字段 user
,那么这条SQL语句会报错,原因是在 SQL 查询中,表名(和列名)属于结构部分,它们不能像普通的参数一样通过预编译的方式进行绑定。#{}
主要用于数据值的传递,它会把占位符替换为实际的参数值,并且通过 JDBC 的预编译机制来避免 SQL 注入问题。由于表名是 SQL 语句的一个组成部分,它不能作为参数直接传入。
6.总结对比
特性 | #{} | ${} |
---|---|---|
用途 | 参数占位符,用于安全地传递参数 | 字符串拼接,用于动态拼接 SQL 语句 |
传递方式 | 使用 PreparedStatement 传递参数 | 直接替换为参数值,拼接到 SQL 中 |
安全性 | 防止 SQL 注入,自动转义 | 容易引起 SQL 注入,存在风险 |
使用场景 | 适用于用户输入、动态查询参数 | 适用于动态生成表名、列名等固定部分 |
自动处理 | 自动类型转换,防止 SQL 注入 | 不做任何处理,直接插入到 SQL 中 |
- 优先使用
#{}
: 如果参数值来自用户输入或不受信任的源,使用#{}
,它会自动处理 SQL 注入问题,确保 SQL 查询的安全。 - 慎用
${}
:${}
应该仅在你确定参数值安全(如常量或静态字段)时使用,且不包含用户输入时使用。否则,避免在 SQL 中拼接任何可能来自用户的动态内容。