为什么MyBatis中常用#{}而不使用${}
作者:Leaton Lee
本文主要介绍了为什么MyBatis中常用#{}而不使用${},文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
前言:一个真实的故事引发的技术思考
去年双十一凌晨2点,某公司用户数据被删,因为一个MyBatis的${}使用失误,导致黑客通过地址栏注入恶意SQL,直接清空了用户表...
这就是我今天要讲的:为什么MyBatis必须用#{},打死都不能用${}! 跟着我左手画个SQL,右手写个防注入,咱们用3个真实案例+6个代码演示+1个完整项目,彻底讲透这个价值百万的安全问题!
一、新手村装备:MyBatis参数传递基础
1.1 参数占位符是什么?
在MyBatis的XML映射文件中,我们经常看到这样的SQL:
SELECT * FROM users WHERE id = #{userId}
这里的#{}
就是MyBatis的参数占位符,相当于JDBC中的PreparedStatement
的 ‘?’
。
1.2 两种占位符对比表
特性 | #{} | ${} |
---|---|---|
参数类型 | 任意类型 | 只能字符串 |
SQL注入 | 安全 | 高危 |
预编译 | 支持 | 不支持 |
参数替换方式 | 带引号替换 | 直接拼接 |
使用场景 | 值传递 | 动态表/列名 |
二、#{}与${}的"孪生兄弟"之谜
2.1 表面相似的代码
先看两个看似相同的查询:
<!-- 使用#{} --> <select id="getUserById" resultType="User"> SELECT * FROM users WHERE id = #{userId} </select> <!-- 使用${} --> <select id="getUserByIdUnsafe" resultType="User"> SELECT * FROM users WHERE id = ${userId} </select>
2.2 实际执行的SQL
假设传入userId=1:
#{}生成的SQL:
SELECT * FROM users WHERE id = ?
参数1会被预编译处理
${}生成的SQL:
SELECT * FROM users WHERE id = 1
直接拼接参数值
三、SQL注入:程序员的午夜凶铃(代码级演示)
3.1 模拟黑客攻击场景
假设有个登录查询:
<!-- 危险写法! --> <select id="login" resultType="User"> SELECT * FROM users WHERE username = '${username}' AND password = '${password}' </select>
黑客输入:
- 用户名:
admin' --
- 密码:随意填
生成的SQL:
SELECT * FROM users WHERE username = 'admin' -- ' AND password = '123456'
效果:成功绕过密码验证,直接登录admin账号!
3.2 使用#{}的正确姿势
<select id="safeLogin" resultType="User"> SELECT * FROM users WHERE username = #{username} AND password = #{password} </select>
此时无论输入什么,参数都会被正确处理,无法注入。
四、执行过程剖析:预编译 vs 字符串拼接
4.1 MyBatis执行流程对比图
graph TD A[接收参数] --> B{占位符类型} B -->|#{}| C[生成PreparedStatement] B -->|${}| D[拼接完整SQL] C --> E[预编译SQL] E --> F[安全执行] D --> G[创建Statement] G --> H[风险执行]
4.2 预编译的三大优势
- 防注入:将参数视为数据而非代码
- 性能提升:重复查询复用编译结果
- 类型安全:自动处理数据类型转换
五、必须使用${}的三种特殊场景
虽然${}很危险,但在某些场景下不得不使用:
5.1 动态表名查询
<select id="selectByTable" resultType="map"> SELECT * FROM ${tableName} WHERE id = #{id} </select>
安全建议:
- 对tableName进行白名单校验
- 使用正则表达式过滤非法字符
// 校验示例 if (!tableName.matches("[a-zA-Z_0-9]+")) { throw new IllegalArgumentException("Invalid table name"); }
5.2 动态排序字段
ORDER BY ${sortField} #{sortOrder}
安全方案:
- 使用枚举限制可排序字段
- 参数映射转换
public enum SafeSortField { CREATE_TIME("create_time"), VIEW_COUNT("view_count"); private final String columnName; // ... }
六、综合实战:电商订单查询系统
6.1 需求分析
- 根据动态条件查询订单
- 支持多字段排序
- 分页查询
6.2 安全实现方案
<select id="searchOrders" resultType="Order"> SELECT * FROM orders <where> <if test="userId != null"> user_id = #{userId} </if> <if test="startDate != null"> AND create_time >= #{startDate} </if> <!-- 更多条件... --> </where> <!-- 安全排序方案 --> <if test="sortBy != null"> ORDER BY <choose> <when test="sortBy == 'TIME'">create_time</when> <when test="sortBy == 'AMOUNT'">total_amount</when> <otherwise>id</otherwise> </choose> ${sortOrder} </if> LIMIT #{pageSize} OFFSET #{offset} </select>
七、灵魂拷问:你真的懂了吗?
- 当需要动态指定查询列时,应该用哪种占位符?
- 为什么说使用${}就像在SQL中开eval()?
- 如何安全地实现动态表头导出功能?
把你的答案写在评论区,抽3位同学送《MyBatis深度解析》电子书!
八、终极总结(保存这张图!)
pie title 占位符使用准则 "#{} 常规参数" : 95% "${} 动态表/列" : 4% "${} 其他场景" : 1%
三条黄金法则:
- 能用#{}坚决不用${}
- 必须用${}时要做好过滤
- 所有用户输入必须使用#{}处理
九、FAQ精选
Q:为什么框架不直接禁用${}?
A:因为确实存在需要SQL动态拼接的场景,框架需要保持灵活性,但安全责任在开发者。
Q:使用#{}会导致性能问题吗?
A:恰恰相反,预编译语句通常会提升性能,因为可以复用执行计划。
Q:如何全局防止滥用?
A:可以通过自定义MyBatis插件,在发现${}时发出警告(需要团队规范配合)。
附录:安全自查清单
- 项目中没有直接使用${}接收用户输入的案例
- 所有动态表/列操作都有白名单校验
- 关键SQL语句都经过安全审核
- 定期进行SQL注入漏洞扫描
到此这篇关于为什么MyBatis中常用#{}而不使用${}的文章就介绍到这了,更多相关MyBatis 常用#{}内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!