mybatis如何防止sql注入?(图解+秒懂+史上最全)

本文 的 原文 地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

尼恩说在前面:

最近大厂机会多了, 在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字+),请参参见原文地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值