【MyBatis】MyBatis中 #{ }和 ${ }的区别

引言

在使用 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 中拼接任何可能来自用户的动态内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值