MyBatis 中 `#` 与 `$` 参数替换的关键区别,懂了就能写出更安全的代码!

246 阅读3分钟

# 和 $ 在 MyBatis 中的区别:安全性与应用场景详解

在 MyBatis 中,#$ 都用于参数替换,但它们在实际使用中有显著的区别,尤其是在安全性和灵活性方面。这两种替换方式各有其特定的使用场景和优劣。下面我们将详细探讨这两者的差异、工作原理以及最佳使用实践。

一、作用和工作方式的不同

#(预编译参数占位符)
# 是 MyBatis 中用于参数占位的方式,实际上它会通过预编译的方式将 SQL 语句和参数分离处理。使用 # 替换的参数会被安全地替换到 SQL 语句中,MyBatis 会自动对参数进行转义,尤其是字符串类型的参数,它会为这些参数自动添加单引号,确保 SQL 执行时参数不会被错误地解析为 SQL 语句的一部分,从而有效避免 SQL 注入攻击。

例如,假设我们有以下 SQL 查询:

SELECT * FROM users WHERE username = #{username}

如果传入的 username 参数为 "admin" OR 1=1 --,MyBatis 会将参数值替换成 admin' OR 1=1 --,并且在执行时不会将其解释为 SQL 语句的一部分。

$(直接替换)
# 不同,$ 是直接将参数的值替换到 SQL 语句中的方式。它不会进行预编译,也不会对参数值进行任何转义。因此,如果参数值本身包含 SQL 语句的关键字或操作符,它就可能会导致 SQL 注入漏洞。例如:

SELECT * FROM users WHERE username = ${username}

假设 username 参数为 admin' OR 1=1 --,执行时 SQL 语句就变成了:

SELECT * FROM users WHERE username = admin' OR 1=1 --'

这个 SQL 语句会绕过身份验证,导致严重的安全问题。

二、安全性

#:防止 SQL 注入

通过预编译的方式,# 保证了参数和 SQL 语句的分离。MyBatis 会自动对字符串进行转义,确保 SQL 注入攻击无法通过参数传入恶意的 SQL 语句。因此,# 是一个安全的选择,尤其是在处理用户输入的情况下,能够有效避免 SQL 注入攻击。

$:易受 SQL 注入攻击

$ 替换方式没有进行任何转义处理,它将参数值直接嵌入 SQL 语句中,存在极大的 SQL 注入风险。如果用户输入恶意 SQL 代码,就可能破坏 SQL 语句的结构,从而对数据库造成潜在威胁。

三、自动添加单引号

#:自动为字符串参数加单引号

当我们使用 # 占位符时,MyBatis 会自动将字符串类型的参数加上单引号,确保 SQL 语句合法。例如:

SELECT * FROM users WHERE username = #{username}

假设 username 的值为 admin,MyBatis 会将 SQL 语句构造为:

SELECT * FROM users WHERE username = 'admin'

$:不会自动加单引号

对于 $,它直接将参数值插入 SQL 语句中,不会对字符串添加单引号。因此,如果我们传入的是字符串类型的参数,可能会导致 SQL 语法错误。例如:

SELECT * FROM users WHERE username = ${username}

如果 usernameadmin,最终执行的 SQL 语句会是:

SELECT * FROM users WHERE username = admin

这显然是一个无效的 SQL 语句,数据库会报错。

四、常见使用场景

#:适用于大多数参数替换场景

# 是 MyBatis 中最常用的参数替换方式,尤其适用于处理用户输入的动态查询条件。无论是文本、数字、日期等类型的参数,都可以安全地使用 # 替换,防止 SQL 注入问题。常见的使用场景包括:

  • 用户输入的查询条件,如用户名、密码等。
  • 动态的 WHERE 子句。
  • 模糊查询等。

例如,查询某个用户的资料:

<select id="findUserByUsername" resultType="User">
    SELECT * FROM users WHERE username = #{username}
</select>

$:用于动态 SQL 生成

$ 主要用于 SQL 的动态生成,尤其是涉及到列名、表名等静态 SQL 的一部分。由于列名和表名并不受参数值的类型约束,因此可以使用 $ 来构建灵活的 SQL 语句。常见的应用场景包括:

  • 动态选择字段(列名)。
  • 动态表名(例如用户选择的表名)。

例如:

<select id="findUserByField" resultType="User">
    SELECT * FROM ${tableName} WHERE ${columnName} = #{value}
</select>

这里,$ 用于动态替换表名和列名,而 # 用于参数值的替换。

五、总结

  • 使用 # 进行参数替换:可以有效防止 SQL 注入攻击,MyBatis 会自动为字符串类型的参数添加单引号,并且参数会以安全的方式被预编译执行。
  • 使用 $ 进行参数替换:需要格外小心,因为它直接将参数值插入 SQL 语句,可能会导致 SQL 注入问题。它通常用于动态生成 SQL 语句的一部分,如表名和列名。

安全编程提示:尽量避免在用户输入中使用 $,尤其是当输入值来自不可信来源时。通常推荐优先使用 #,只有在确实需要动态替换表名或列名时,才使用 $

示例代码

public interface UserMapper {

    // 使用#来防止SQL注入
    @Select("SELECT * FROM users WHERE username = #{username}")
    User findByUsername(String username);

    // 使用$来动态替换表名
    @Select("SELECT * FROM ${tableName} WHERE ${columnName} = #{value}")
    User findByDynamicField(@Param("tableName") String tableName,
                            @Param("columnName") String columnName,
                            @Param("value") String value);
}

在以上代码中,findByUsername 使用了 #,确保 SQL 安全;而 findByDynamicField 使用了 $ 来动态替换表名和列名,适用于需要动态生成 SQL 语句的场景。

通过合理选择 #$,可以使 MyBatis 的 SQL 执行既灵活又安全。