MyBatis中#{}和${}的深度解析:SQL注入与动态拼接的终极抉择

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);  // 安全!参数被严格处理

执行流程

  1. 将SQL语句编译为模板
  2. 参数作为独立数据传入
  3. 数据库引擎严格区分指令和数据
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 users
    ORDER BY ${sortColumn} ${sortOrder}
</select>
安全规范:
  1. 白名单校验:在Java代码中校验传入的元数据
    // 表名白名单校验
    Set<String> validTables = Set.of("log_2023","log_2024");
    if(!validTables.contains(tableName)) {
        throw new IllegalArgumentException("Invalid table name");
    }
    
  2. 避免用户输入:动态参数应来自系统内部,而非前端直接传入

五、性能对比:# vs $的隐藏差异

操作#{}${}
SQL编译首次编译模板,后续复用每次生成全新SQL
数据库缓存相同SQL模板可复用执行计划每次被视为不同SQL,无法复用
执行10万次编译1次 + 执行10万次编译10万次 + 执行10万次
典型耗时≈1.5秒≈15秒(10倍差距!)

实测结论:高并发场景下,#{}的性能优势极为明显!


六、黄金法则:如何选择符号

  1. 优先使用#{}

    • WHERE条件中的值
    • INSERT/UPDATE的字段值
    • 所有用户输入参数
  2. 谨慎使用${}

    • 动态表名/列名
    • ORDER BY排序子句
    • SQL关键字(如LIMIT)
    • 必须确保参数值内部可控!
  3. 绝对禁止

    <!-- 禁止将用户输入直接用于$ -->
    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)定期检测漏洞。

技术讨论:你在项目中遇到过哪些${}引发的安全问题?欢迎评论区分享避坑经验!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值