本文 的 原文 地址
原始的内容,请参考 本文 的 原文 地址
尼恩说在前面:
最近大厂机会多了, 在45岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、shein 希音、shopee、百度、网易的面试资格,遇到很多很重要的MyBatis 面试题。
前几天一个小伙面京东,被问 : MyBatis 如何实现 “面向接口” 查询的 ? mybatis 的用接口怎么实现查询的?
答案原文。
最近一个小伙伴拿了滴滴机会,进了 滴滴二面挂了!
被 问“mybatis如何防止sql注入?”。明明答对了的 ,结果还是挂了…
小伙伴面试完了之后,来求助尼恩。
那么,遇到 这个问题,该如何才能回答得很漂亮,才能 让面试官刮目相看、口水直流。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增。
尼恩给大家梳理4 抡 连环 暴击,帮助大家 毒打面试官,逆天翻盘。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典》V175版本PDF集群,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,后台回复:领电子书
第1抡暴击:MyBatis 防 SQL 注入:安全写法+ 危险写法 详解
先说结论
- 使用
#{}是安全的: 预编译参数绑定, PreparedStatement 防止 SQL 注入,推荐在绝大多数场景下使用。 - 使用
${}是危险的: 字符串拼接,存在严重 SQL 注入风险,仅限用于动态表名、列名等无法用参数占位的特殊场景,并必须配合白名单校验。 - 关键原则:用户输入永远不要参与 SQL 结构拼接,只能作为数据值传入。
再介绍: 为什么 #{} 能防注入?${} 却不能?
1. SQL 注入的本质是什么?
SQL 注入是指攻击者通过在输入中插入恶意 SQL 片段,改变原始 SQL 的逻辑结构。例如:
SELECT * FROM users WHERE username = 'Alice' AND password = '123456'
如果用户名输入为 ' OR '1'='1,而程序直接拼接字符串:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '...'
此时 '1'='1' 恒为真,条件始终成立,可能导致所有用户数据被查出。
2. 预编译参数绑定 #{} 的工作原理: Prepared Statement
当你在 MyBatis 中使用 #{username} 时:
<select id="selectUser" resultType="User">
SELECT * FROM users WHERE username = #{username}
</select>
MyBatis 实际生成的是 JDBC 的 预编译语句(PreparedStatement):
String sql = "SELECT * FROM users WHERE username = ?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setString(1, userInput); // 用户输入作为参数设置,不参与SQL拼接
✅ 关键点:SQL 结构在执行前已固定,用户输入只是“填空”的值,不会被数据库解析为 SQL 代码。
3. 字符串拼接 ${} 的工作原理:字符串替换(String Concatenation)
当你使用 ${username} 时:
<select id="selectUser" resultType="User">
SELECT * FROM users WHERE username = '${username}'
</select>
MyBatis 会将 ${username} 直接替换成用户输入的内容,相当于:
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);
❌ 风险点:用户输入直接参与 SQL 字符串构建,可构造闭合引号、追加额外条件,实现任意 SQL 执行。
4. #{} 和 ${}使用的案例对比介绍
下面以两个写法为例,展示其执行流程差异。
使用 #{} 的安全流程
<select id="selectUserByCredential" resultType="User">
SELECT * FROM users
WHERE username = #{username} AND password = #{password}
</select>
✅ 结果:即使输入包含 ' 或其他符号,也只是当作字符串处理,无法改变 SQL 结构。
使用 ${} 的危险流程
<select id="selectUser" resultType="User">
SELECT * FROM users WHERE username = '${username}'
</select>
❌ 结果:攻击者绕过条件限制,获取非授权数据。
5. 但是: ${}?在某些动态 SQL 场景中是必要的
虽然 ${} 有风险,但在某些动态 SQL 场景中是必要的。
以下是唯一允许使用 ${} 的情况及其防护措施。
✅ 合理使用场景(需严格控制)
| 场景 | 示例 | 是否可用 ${} |
|---|---|---|
| 动态表名 | SELECT * FROM ${tableName} | ✅ 可用,但需白名单校验 |
| 动态排序字段 | ORDER BY ${columnName} | ✅ 可用,但需白名单校验 |
| 动态列名(极少见) | SELECT ${colName} FROM users | ✅ 可用,但需白名单校验 |
使用${}? 必须采取的安全措施
当必须使用 ${} 时,绝对禁止直接传入用户原始输入,必须进行以下校验:
// 白名单校验示例:只允许特定列用于排序
public String validateSortColumn(String column) {
Set<String> allowedColumns = Set.of("id", "username", "created_time", "status");
if (!allowedColumns.contains(column)) {
throw new IllegalArgumentException("Invalid sort column: " + column);
}
return column; // 返回合法值,供 ${} 使用
}
Mapper 调用:
<select id="selectUsersOrderBy" resultType="User">
SELECT * FROM users ORDER BY ${validatedColumn}
</select>
⚠️ 注意:${validatedColumn} 接收的是经过校验的内部变量,不是前端直接传来的参数!
6. 绝对禁止的危险用法
<!-- 错误1:用户输入直接拼接 -->
<select id="badExample1" resultType="User">
SELECT * FROM users WHERE username = '${username}'
</select>
<!-- 错误2:未校验的动态表名 -->
<select id="badExample2" resultType="User">
SELECT * FROM ${userInputTable}
</select>
<!-- 错误3:拼接复杂表达式 -->
<select id="badExample3" resultType="User">
SELECT * FROM users WHERE ${arbitraryCondition}
</select>
这些写法极易导致删库、拖库等严重后果。
7.示例 ${} 配套 白名单校验实现
// 表名白名单校验工具类
public class SqlSafeUtils {
// 允许使用的表名白名单
private static final Set<String> TABLE_WHITELIST = new HashSet<>(Arrays.asList("user", "order", "product"));
// 允许使用的排序字段白名单
private static final Set<String> COLUMN_WHITELIST = new HashSet<>(Arrays.asList("id", "username", "create_time", "price"));
// 表名校验
public static String validateTableName(String tableName) {
if (StringUtils.isBlank(tableName) || !TABLE_WHITELIST.contains(tableName.trim())) {
throw new IllegalArgumentException("非法表名:" + tableName);
}
return tableName.trim();
}
// 排序字段校验
public static String validateOrderByColumn(String columnName) {
if (StringUtils.isBlank(columnName) || !COLUMN_WHITELIST.contains(columnName.trim())) {
throw new IllegalArgumentException("非法排序字段:" + columnName);
}
return columnName.trim();
}
}
// 业务层调用示例
public List<User> selectUsersOrderBy(String columnName, String sortDirection) {
// 先校验,再传入Mapper
String safeColumn = SqlSafeUtils.validateOrderByColumn(columnName);
String safeDirection = "DESC".equalsIgnoreCase(sortDirection) ? "DESC" : "ASC";
return userMapper.selectUsersOrderBy(safeColumn, safeDirection);
}
8 第一轮暴击的 小结
- 能用
#{}就不用${}${}只做结构动态化,不做数据传值- 凡是
${},必加白名单- 用户输入不拼 SQL,安全才有保障
遵循以上规则,即可有效防御 MyBatis 场景下的 SQL 注入风险。
尼恩总结 第1抡暴击 路径
【这轮暴击覆盖 核心点】:
- 剖析 MyBatis 防 SQL 注入的底层机制、
- 指出 PreparedStatement 的核心防护逻辑
- 拆解从 Mapper 接口调用到 JDBC 执行的完整链路
【这轮暴击的欠缺和不足】:
- 对“为什么不能完全依赖框架默认行为”缺乏批判性反思;
- 未触及动态 SQL 场景下开发者误用
${}导致的安全盲区,暴露了在复杂场景中风险预判能力的不足
【面试官的表情变化】:
从最初的微微点头、身体前倾表现出兴趣
到听到流程拆解时轻声“嗯”表示认可
【开始 下一轮 暴击 】:
- MyBatis 防 SQL 注入的核心原理
- MyBatis 内部如何实现预编译 底层源码

第2抡暴击:MyBatis 防 SQL 注入的核心原理
MyBatis 能有效防止 SQL 注入,根本原因在于:它使用 JDBC 的 PreparedStatement 预编译机制,将 SQL 结构与参数数据分离。
- SQL 模板在执行前就已固定,数据库会预先解析并编译该结构。
- 用户传入的参数不会拼接到 SQL 中,而是通过占位符
?安全传递。 - 即使参数包含恶意内容(如
' OR '1'='1),也会被当作普通字符串值处理,不会改变原有 SQL 逻辑。
✅ 关键保障:SQL 结构不可变 + 参数安全绑定 = 抵御 SQL 注入。
一、原理拆解:为什么预编译能防注入?
1. SQL 注入的本质是什么?
当用户输入被直接拼接进 SQL 字符串时,可能篡改原始语义:
-- 正常查询
SELECT * FROM users WHERE username = 'admin'
-- 恶意输入 "admin' OR '1'='1"
SELECT * FROM users WHERE username = 'admin' OR '1'='1'
-- 结果:返回所有用户!
2. PreparedStatement 如何解决这个问题?
使用预编译后,SQL 是这样执行的:
String sql = "SELECT * FROM users WHERE username = ?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setString(1, "admin' OR '1'='1"); // 参数作为值传入
此时数据库看到的是:
SELECT * FROM users WHERE username = ?
-- 执行时参数被视为一个整体字符串,等价于:
-- username = 'admin'' OR ''1''=''1'
⚠️ 数据库不会解析参数中的 SQL 关键字,只做精确匹配。
二、 MyBatis 内部如何实现预编译?
以下是 MyBatis 从接口调用到 SQL 执行的完整流程,按阶段划分:
1. 解析阶段(应用启动时)
MyBatis 在初始化时解析 XML 或注解中的 SQL 语句。
例如:
<select id="selectUser" parameterType="map">
SELECT * FROM users WHERE username = #{username}
</select>
会被解析成:
- SQL 模板:
SELECT * FROM users WHERE username = ? - 参数映射:
#{username}→ 第1个占位符 - 存储位置:
MappedStatement对象中,由SqlSource生成BoundSql
📌 关键组件:
- SqlSource:负责生成带
?的预编译 SQL。 - BoundSql:最终要执行的 SQL 对象,包含:
getSql():替换后的 SQL 文本(含?)- 参数映射信息(用于后续设置值)
2. 执行阶段(运行时调用)
(1) 代理拦截:MapperProxy
当你调用 userMapper.selectUser("admin"),实际是调用了 MapperProxy 的代理方法。
作用:
- 拦截接口调用
- 获取方法名、参数
- 转交给 MapperMethod 处理
(2) 方法封装:MapperMethod
MapperMethod 将方法调用封装成可执行指令,决定走查询还是更新流程,并准备参数和 SQL 标识。
然后通过 SqlSession 发起请求。
(3) 会话调度:SqlSession → Executor
SqlSession 是对外统一入口,内部委托给 Executor(默认 SimpleExecutor)执行操作。
(4) 语句准备:StatementHandler 创建预编译对象
StatementHandler 负责创建真正的 JDBC Statement。
默认实现是 PreparedStatementHandler,其核心行为如下:
public class PreparedStatementHandler extends BaseStatementHandler {
@Override
public Statement instantiateStatement(Connection connection) throws SQLException {
// 使用 connection.prepareStatement 创建预编译语句
String sql = boundSql.getSql(); // 已经是 ? 形式
return connection.prepareStatement(sql);
}
@Override
public void parameterize(Statement statement) throws SQLException {
// 调用 ParameterHandler 设置参数
parameterHandler.setParameters((PreparedStatement) statement);
}
}
对应流程:
instantiateStatement()→ 向数据库发送 SQL 模板,进行预编译parameterize()→ 触发参数设置
(5) 参数安全绑定:ParameterHandler
ParameterHandler(通常是 DefaultParameterHandler)将 Java 参数转换为 JDBC 类型,并设置到 PreparedStatement 的占位符上。
这个过程完全避免字符串拼接,确保参数以“数据”而非“代码”形式存在。
尼恩总结 第2抡暴击 路径
【这轮暴击覆盖 核心点】:
深入剖析 MyBatis 防注入的底层机制、
PreparedStatement 的安全本质、
SqlSource 与 BoundSql 的作用、
MapperProxy 代理流程、
StatementHandler 的预编译创建逻辑
还原了 MyBatis 执行链路的技术细节
【这轮暴击的欠缺和不足】:
- 尚未跃迁至“知其所以然”的战略层面
【面试官的表情变化】:
从最初的微微点头认可技术扎实,
听到核心原理,坐直身体,表现出兴趣
【开始 下一轮 暴击 】:
引入“防御纵深”理念,对比 Hibernate 等 ORM 的全封闭策略与 MyBatis 半开放架构的适用场景差异;
深入探讨为何优秀的框架设计往往不是消灭风险,而是将风险暴露得足够清晰并引导开发者做出正确选择;

第3抡暴击:MyBatis 防 SQL 注入的底层设计原理与设计哲学
MyBatis 通过 默认使用 #{} 实现预编译参数绑定,从根本上防御 SQL 注入攻击。
其设计遵循“安全优先”的原则,在灵活性与安全性之间取得良好平衡:
- ✅ 安全性高:
#{}对应 PreparedStatement,参数作为纯数据传入,无法改变 SQL 结构。 - ⚠️ 灵活性代价:
${}支持动态拼接 SQL(如表名、排序字段),但需开发者自行校验,存在注入风险。 - 🎯 设计目标:将 SQL 控制权交给开发者的同时,提供安全的默认路径,降低误用门槛。
只要正确使用 #{},绝大多数场景下可完全避免 SQL 注入问题。
关键设计哲学
(1)安全默认值:约定优于配置
MyBatis 默认推荐并鼓励使用 #{},它对应的是安全的预编译方式。
只有显式使用 ${} 才会触发字符串替换,这要求开发者主动“跳出”安全区。
类比:就像汽车出厂默认系好安全带,你要故意解开才能冒险。
(2)关注点分离:SQL 结构 vs 参数数据
- SQL 结构:由开发者在 XML 或注解中编写,属于可信代码。
- 参数数据:来自用户请求或业务逻辑,属于不可信输入。
MyBatis 明确区分这两者,确保参数只能填充到已定义的位置,不能修改 SQL 的语法树。
(3)灵活与安全的平衡架构
MyBatis 内部通过以下组件解耦处理流程:
- SqlSource:负责解析 SQL 语句,生成可执行的 SQL 模板。
- ParameterHandler:处理
#{}参数的设置,调用PreparedStatement.setXXX()方法。 - Executor:执行最终的 SQL。
这种设计使得框架既能保证安全机制可插拔扩展,又允许 ${} 存在以支持极少数动态需求(如分表、动态 ORDER BY)。
#{} 与 ${} 的本质区别
| 特性 | #{} | ${} |
|---|---|---|
| 是否预编译 | 是(PreparedStatement) | 否(字符串拼接) |
| 参数处理方式 | 占位符替换,安全绑定 | 直接文本替换,易被注入 |
| 使用场景 | 绝大多数参数传入(WHERE、INSERT VALUES 等) | 动态表名、列名、ORDER BY 字段等 |
| 安全性 | 高,自动防御注入 | 低,需手动过滤或白名单校验 |
灵活性 vs 安全责任 的 平衡:
优势总结
(1) 绝对安全(前提正确使用)
- 基于数据库层面的预编译机制,从根源阻断注入可能。
- 参数永不参与 SQL 语法构建。
(2) 性能更优
- 数据库可缓存预编译计划(Statement Cache),多次执行相同 SQL 无需重复解析优化。
- 尤其适合高频查询场景。
(3) 开发灵活可控
- 相比 Hibernate 等全自动 ORM,MyBatis 允许手写 SQL,便于优化复杂查询。
- DBA 友好,SQL 审计清晰。
MyBatis 的局限与挑战
(1) 依赖开发者规范
- 框架无法强制禁止
${}使用。 - 新人若不了解差异,容易误用导致漏洞。
示例错误:
<!-- 危险! -->
<select id="dynamicQuery" resultType="User">
SELECT * FROM ${tableName} WHERE age > #{age}
</select>
若 tableName 来自用户输入且未校验,即可注入任意表名。
(2) 动态场景需额外防护
-
对必须使用
${}的情况(如分库分表),需配合白名单校验:if (!Arrays.asList("user_2024", "user_2025").contains(tableName)) { throw new IllegalArgumentException("Invalid table name"); } -
增加了开发者的安全编码负担。
总结对比要点
- MyBatis vs JDBC:MyBatis 封装了
Connection.prepareStatement()、setParameters()等重复操作,减少人为失误,提升开发效率。 - MyBatis vs Hibernate:
- Hibernate 更“安全”是因为几乎不写原生 SQL,但 HQL 仍可能因拼接字符串导致注入。
- MyBatis 更贴近数据库,适合需要精细控制 SQL 的项目(如金融、大数据报表系统)。
第3抡暴击小结
MyBatis 采用“安全为默认,灵活作例外”的设计哲学,通过 #{} 强制预编译、${} 保留动态能力的方式,在 SQL 安全性和开发自由度之间取得了出色平衡。
只要开发者理解其工作原理,并遵守基本规范,就能在享受高效开发的同时,有效抵御 SQL 注入威胁。对于重视 SQL 性能与可控性的 Java 项目,MyBatis 依然是极具竞争力的选择。
尼恩总结 第3抡暴击 路径
【这轮暴击覆盖 核心点】:
- MyBatis 防注入机制基于预编译与参数绑定,从设计哲学层面体现“安全优先”原则;
- 框架通过 SqlSource、ParameterHandler 等组件实现关注点分离,保障安全性与灵活性的架构平衡;
- 分析
#{}与${}的语义差异,本质是“数据”与“代码”的边界划分,体现了 ORM 对 SQL 控制权的精细管理
【这轮暴击的欠缺和不足】:
- 需要提升到 规范和 团队能力提高的层面,
- 为sql的安全 构筑一道 坚固的屏障,
- 没有 系统性风控思维的跃迁
【面试官的表情变化】:
从最初的微微点头认可技术扎实,
到中间阶段坐直身体表现出兴趣
再到听到设计哲学时短暂两眼放光
期待看到更深层的洞见
【开始 下一轮 暴击 】:
- 展现从个人编码意识到系统性风控思维的跃迁
- mybatis 防 sql注入规范手册< /实践手册>

第4抡暴击:mybatis 防 sql注入规范手册< /实践手册>
… 略5000字+
…由于平台篇幅限制, 剩下的内容(5000字+),请参参见原文地址
6598

被折叠的 条评论
为什么被折叠?



