MyBatis中#{}
和${}
的深度解析:SQL注入与动态拼接的终极抉择
摘要:在MyBatis的Mapper.xml文件中,
#{}
和${}
这两个看似简单的符号,却隐藏着SQL安全与性能的核心秘密。本文将深入剖析它们的底层差异,并通过真实场景演示如何正确选择,避免致命的安全漏洞!
一、符号初探:表面相似,本质不同
在MyBatis的SQL编写中,我们经常看到这样的写法:
<!-- 使用# -->
<select id="getUser" resultType="User">SELECT * FROM user WHERE id = #{id}
</select><!-- 使用$ -->
<select id="getUser" resultType="User">SELECT * FROM user WHERE name = ${name}
</select>
表面看:两者都用于参数替换
本质区别:它们的处理机制天差地别!
特性 | #{} | ${} |
---|---|---|
处理方式 | 预编译参数(PreparedStatement) | 字符串直接替换 |
防SQL注入 | ✅ 安全 | ❌ 高风险 |
数据类型转换 | 自动类型转换 | 需手动加引号 |
适用场景 | 值传递(WHERE条件等) | 动态SQL片段(表名等) |
二、底层原理:安全与危险的根源
1. #{}
的预编译机制(安全卫士)
// MyBatis实际执行代码(简化版)
PreparedStatement ps = conn.prepareStatement("SELECT * FROM user WHERE id=?");
ps.setInt(1, 5); // 安全!参数被严格处理
执行流程:
- 将SQL语句编译为模板
- 参数作为独立数据传入
- 数据库引擎严格区分指令和数据
2. ${}
的字符串替换(危险陷阱)
// 假设传入 name = "admin' OR '1'='1"
String sql = "SELECT * FROM user WHERE name=" + name;
// 最终SQL:SELECT * FROM user WHERE name='admin' OR '1'='1'
注入风险:攻击者可通过精心构造参数执行任意SQL!
三、实战对比:当$
遭遇SQL注入攻击
场景:用户登录验证
<!-- 危险写法 -->
<select id="login" resultType="User">SELECT * FROM users WHERE username = ${username} AND password = ${password}
</select>
攻击者输入:
username = "admin' -- "
password = "anything"
生成的致命SQL:
SELECT * FROM users
WHERE username = 'admin' -- ' AND password = 'anything'
结果:攻击者无需密码直接登录管理员账户!
修复方案(改用#
):
<select id="login" resultType="User">SELECT * FROM users WHERE username = #{username} AND password = #{password}
</select>
此时攻击输入将被转义为:
WHERE username = 'admin'' -- ' AND ...
数据库会严格查找用户名为 admin' --
的记录,攻击失效!
四、${}
的正确打开方式:动态元数据操作
虽然${}
有风险,但在特定场景下不可替代:
场景1:动态表名
<select id="getLogsByTable" resultType="Log">SELECT * FROM ${tableName} WHERE year = #{year}
</select>
注:表名是SQL指令的一部分,无法使用预编译占位符
场景2:动态排序
<select id="getUsers" resultType="User">SELECT * FROM usersORDER BY ${sortColumn} ${sortOrder}
</select>
安全规范:
- 白名单校验:在Java代码中校验传入的元数据
// 表名白名单校验 Set<String> validTables = Set.of("log_2023","log_2024"); if(!validTables.contains(tableName)) {throw new IllegalArgumentException("Invalid table name"); }
- 避免用户输入:动态参数应来自系统内部,而非前端直接传入
五、性能对比:#
vs $
的隐藏差异
操作 | #{} | ${} |
---|---|---|
SQL编译 | 首次编译模板,后续复用 | 每次生成全新SQL |
数据库缓存 | 相同SQL模板可复用执行计划 | 每次被视为不同SQL,无法复用 |
执行10万次 | 编译1次 + 执行10万次 | 编译10万次 + 执行10万次 |
典型耗时 | ≈1.5秒 | ≈15秒(10倍差距!) |
实测结论:高并发场景下,
#{}
的性能优势极为明显!
六、黄金法则:如何选择符号
-
优先使用
#{}
- WHERE条件中的值
- INSERT/UPDATE的字段值
- 所有用户输入参数
-
谨慎使用
${}
- 动态表名/列名
- ORDER BY排序子句
- SQL关键字(如LIMIT)
- 必须确保参数值内部可控!
-
绝对禁止
<!-- 禁止将用户输入直接用于$ --> WHERE username = ${userInput} ❌
七、扩展技巧:#
的高级用法
1. 类型处理器指定
<!-- 强制使用String类型处理器 -->
#{age, javaType=int, jdbcType=NUMERIC}
2. 日期格式转换
#{createTime, jdbcType=TIMESTAMP, pattern="yyyy-MM-dd"}
3. 非空校验
<!-- 当email为空时设置默认值 -->
#{email, jdbcType=VARCHAR, default='no-email@domain.com'}
结语
#{}
和${}
的选择本质是安全与灵活性的权衡:
#{}
是默认首选,保障安全与性能${}
是特定场景下的"手术刀",需严格管控
牢记:一次错误的
${}
使用可能导致整个系统沦陷!建议在团队中制定《SQL编写规范》,并配合SQL扫描工具(如SQLMap)定期检测漏洞。
技术讨论:你在项目中遇到过哪些${}
引发的安全问题?欢迎评论区分享避坑经验!