在 mybatis 的面试和日常开发中,参数占位符 #{} 和 ${} 的区别是绕不开的核心考点。很多同学只知道“#{} 安全,${} 不安全”,但其背后的底层原理和适用场景却一知半解。本文将通过底层分析、日志演示以及 sql 注入实验,带你彻底搞定这个知识点。
一、 本质区别速览
| 特性 | #{}(井号) | ${}(刀乐/美元符号) |
|---|---|---|
| 底层原理 | jdbc 预编译占位符 ? | 纯字符串拼接,直接替换 |
| sql 注入 | 安全,自动转义特殊字符 | 危险,容易被恶意篡改 |
| 单引号处理 | 自动添加,无需手动处理 | 不自动加,字符串必须手动加 '' |
| 性能 | 高(预编译 sql 可重复利用) | 低(每次都需要重新解析) |
| 使用场景 | 99% 的业务参数(where/set 值) | sql 结构关键字(表名、排序字段) |
二、 核心原理详解
1. #{}:预编译模式(推荐)
当 mybatis 遇到 #{xxx} 时,它会将 sql 发送到数据库进行预编译。在执行阶段,再通过 preparedstatement 设置参数。
- 示例代码:
@select("select * from user_info where username = #{name}")
userinfo querybyname(string name);
- 打印日志(重点):
通过日志可以观察到,sql 中参数部分是?占位符:prepare: select * from user_info where username = ? - 优势:由于 sql 结构已固定,传入的参数只会被当作“值”处理,不会破坏 sql 语义,从而彻底杜绝 sql 注入。
2. ${}:字符串拼接模式(慎用)
${} 会在 sql 执行前,直接把参数原封不动地替换进 sql 语句中。
- 示例代码(报错预警):
@select("select * from user_info where username = ${name}")
userinfo querybyname(string name);
- 运行结果:如果你传入
admin,生成的 sql 是where username = admin。因为缺少单引号,数据库会报错。 - 正确写法:必须手动加引号:
'${name}'。
三、 sql 注入(必考面试点)
sql 注入是指攻击者通过在输入框中填入 sql 片段,篡改原有逻辑的行为。
注入场景:免密登录
假设我们有一段使用 ${} 的危险代码:
select * from user where username = '${name}' and pwd = '${pwd}'
- 正常操作:传入用户名
admin,密码123。 - 攻击操作:攻击者在用户名框输入
admin' --,密码随便写。 - 最终生成的 sql:
select * from user where username = 'admin' -- ' and pwd = 'xxx'
在 sql 中,-- 代表注释。这意味着 and pwd = ... 的逻辑被直接注销掉了!攻击者无需密码即可直接以管理员身份登录。
结论:绝不能使用 ${} 接收用户输入的参数!
四、 sql 注入场景演示
控制层:usertestcontroller
注意自己所写的类位置以及包
import com.example.demo.model.userinfo;
import com.example.demo.service.userservice;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.web.bind.annotation.requestmapping;
import org.springframework.web.bind.annotation.restcontroller;
@restcontroller
public class usertestcontroller {
@autowired
private usertestservice userservice;
@requestmapping("/login")
public boolean login(string name, string password) {
userinfo userinfo = userservice.queryuserbypassword(name, password);
if (userinfo != null) {
return true;
}
return false;
}
}业务层:usertestservice
import com.example.demo.mapper.userinfomapper;
import com.example.demo.model.userinfo;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.stereotype.service;
@service
public class usertestservice {
@autowired
private userinfotestmapper userinfomapper;
public userinfo queryuserbypassword(string name, string password) {
list<userinfo> userinfos = userinfomapper.queryuserbypassword(name, password);
if (userinfos != null && userinfos.size() > 0) {
return userinfos.get(0);
}
return null;
}
}数据层:userinfotestmapper
import com.example.demo.model.userinfo;
import org.apache.ibatis.annotations.*;
import java.util.list;
@mapper
public interface userinfotestmapper {
@select("select username, `password`, age, gender, phone from user_info where username= '${name}' and password='${password}' ")
list<userinfo> queryuserbypassword(string name, string password);
}启动服务,访问:http://127.0.0.1:8080/login?name=admin&password=admin
程序正常运行
接下来访问sql注⼊的代码:
password设置为' or 1='1
拼接为:http://127.0.0.1:8080/login?name=admin&password=’ or 1='1
注意看网页地址!(百分号和空格进行了url编码,因为url不允许直接添加特殊字符)
五、 ${} 的“唯一”合法使用场景
既然 ${} 这么危险,为什么不废掉它?因为它在处理 sql 结构时无可替代:
- 动态表名:
select * from ${tablename}(#{}无法用于表名,因为预编译不支持表名占位)。 - 动态排序字段:
order by ${column} ${ordertype}(如按id或create_time排序,且指定asc/desc)。
安全建议:在使用这些场景时,必须在 service 层做白名单校验,确保传入的列名或表名是合法的。
六、 开发规范口诀
为了方便记忆,我们可以总结为一段口诀:
井号预编译安全自带引号,刀乐拼接危险手动加引号;
业务参数全用井号,结构关键字才用刀乐。
一句话总结:
平时开发无脑用 #{};只有在需要动态传递表名、列名、排序关键字且已经做好安全过滤的情况下,才考虑使用 ${}。
到此这篇关于mybatis中#{}和${}的区别及底层原理、适用场景和安全风险的文章就介绍到这了,更多相关mybatis中#{}和${}的区别内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论