在MyBatis中防止SQL注入的核心,在于正确理解和使用#{}与${}这两种参数占位符。最直接的答案是:永远优先使用#{},它能自动进行预编译和参数化处理,从根本上杜绝SQL注入;而${}是直接的字符串拼接,存在极高的安全风险,仅在极少数特殊场景下谨慎使用。两者的差异并非简单的语法不同,而是涉及SQL执行原理和安全级别的本质区别。#{}在底层会生成一个PreparedStatement,参数会被安全地绑定,而${}只是简单的文本替换,会将传入值原封不动地拼接进SQL语句。
一、 技术原理深度剖析:#{}的“防弹衣”与${}的“拼接刀”
#{}的工作机制是预编译。当MyBatis看到#{}时,它会向数据库发送一个带占位符(通常为?)的SQL模板。例如,SELECT * FROM user WHERE id = #{userId}会被转换为SELECT * FROM user WHERE id = ?。数据库会先编译这个SQL结构,之后传入的参数值(如userId=5)会作为纯粹的数据,通过JDBC驱动安全地“绑定”到占位符上。在这个过程中,无论参数内容是什么,即使它包含恶意的SQL片段(如1 OR 1=1),也只会被当作一个普通的字符串或数字值来处理,而不会被解析为SQL指令。
// 安全的使用方式SELECT * FROM user WHERE name = #{name}// 执行时,传入 name = "Robert'); DROP TABLE users; --"
// 实际执行的SQL: SELECT * FROM user WHERE name = ?
// 参数绑定后,数据库查找名字为"Robert'); DROP TABLE users; --"的用户,不会造成破坏。${}的工作机制是字符串拼接(Statement拼接)。MyBatis会将${}中的内容直接替换到SQL语句中,生成一个完整的SQL字符串再发送给数据库。例如,SELECT * FROM ${tableName} WHERE status = #{status},如果tableName的值为user,则生成的SQL为SELECT * FROM user WHERE status = ?。问题在于,${}的内容是原样替换,如果这个值来自用户输入且未经验证,攻击者就可以注入任意SQL代码。
// 危险的使用方式SELECT * FROM ${tableName} WHERE id = #{id}// 如果攻击者控制 tableName = "user; DELETE FROM user; --"
// 最终SQL将变成: SELECT * FROM user; DELETE FROM user; -- WHERE id = ?
// 这将导致灾难性的数据丢失。二、 ${}的正确使用场景:知其险,限其用
既然${}如此危险,为何MyBatis还要保留它?因为它提供了动态SQL的终极灵活性,在完全可信或严格控制的场景下不可或缺。其合法使用场景必须遵循一个原则:参数值必须是开发人员编写或经过严格白名单校验的,绝不允许直接来自前端用户输入。
场景1:动态指定数据库表名或列名。这些结构名无法使用预编译占位符,必须通过${}动态拼接。但必须确保值来自系统配置或枚举,而非用户输入。
// 安全示例:表名来自配置文件或枚举SELECT * FROM ${logTablePrefix}_202310 WHERE type = #{type}// logTablePrefix 应在服务层硬编码或从安全配置源读取。场景2:实现动态排序(ORDER BY)。排序字段和方向有时需要前端指定,但必须进行严格校验。绝不能直接将前端参数放入${}。
// 相对安全的做法:通过代码进行白名单映射
// 服务层代码
MapcolumnMap = new HashMap<>();
columnMap.put("createTime", "create_time");
columnMap.put("name", "username");
String safeOrderBy = columnMap.getOrDefault(frontendOrderField, "create_time") + " " + (frontendOrderDir.equals("desc") ? "DESC" : "ASC");
// XML映射SELECT * FROM user
ORDER BY ${safeOrderBy}// 注意:排序方向(ASC/DESC)的校验同样重要。场景3:拼接SQL片段。在构建极其复杂的动态SQL时,可能需要插入一个完整的、可复用的SQL片段(如一个子查询)。这个片段必须是预先在代码中定义好的常量字符串。
三、 企业级代码规范与防御策略
仅知道原理和场景是不够的,必须将其固化为团队规范和开发习惯。
规范1:强制代码审查与静态扫描。在团队中建立铁律:所有在MyBatis XML或注解中出现的${}必须经过重点审查。可以引入如SonarQube等静态代码分析工具,配置规则直接标记出未经白名单校验的${}使用,并将其视为高危漏洞。
规范2:建立安全的动态SQL构建体系。对于必须使用${}的场景,应封装统一的工具类或使用MyBatis提供的动态SQL标签(如<if>、<choose>)来替代大部分字符串拼接需求。
// 优先使用MyBatis动态SQL标签处理条件分支,而非拼接字符串SELECT * FROM userAND name like concat('%', #{name}, '%')AND status = #{status}ORDER BY create_time DESC规范3:实施输入验证与白名单机制。对于任何可能流向${}参数的数据,实施“默认拒绝”策略。使用白名单进行严格校验,只允许预定义的、安全的选项通过。例如,对于动态表名,可以预先定义一个允许的表名集合;对于排序字段,建立字段名映射字典。
规范4:最小权限原则。连接数据库的账号应遵循最小权限原则,只授予应用所需的最小数据操作权限(如SELECT、INSERT),避免使用拥有DROP、DELETE等危险权限的数据库管理员账号。这样即使发生注入,也能将破坏限制在可控范围。
规范5:日志与监控。完整记录所有数据库操作日志,尤其是包含动态部分(如${})的SQL执行上下文。监控异常的SQL执行模式(如短时间内大量全表扫描、非业务时段的结构变更操作),以便及时发现潜在的攻击行为。
四、 进阶思考:ORM框架的“双刃剑”效应
MyBatis作为一款“半自动化”ORM框架,在提供灵活性的同时,也将部分安全责任转移给了开发者。这与全自动化的框架(如Hibernate的JPQL/Criteria)形成对比。后者通常更严格地强制使用参数化查询,但可能在处理极端动态SQL时显得笨拙。因此,选择MyBatis往往意味着团队需要更高的安全素养和规范意识。
更深一层看,SQL注入防御的本质是“将代码与数据分离”。#{}严格遵循了这一原则,将SQL结构(代码)和参数值(数据)分开处理。而${}则模糊了这一边界。在现代应用开发中,这一原则也应扩展到所有层面,例如:使用安全的API、对数据进行恰当的编码输出(防止XSS)、使用安全的第三方库等,共同构建纵深防御体系。
总之,防止MyBatis中的SQL注入,不是一个单纯的技术选型问题,而是一个涉及编码规范、安全开发流程、团队培训和工具链建设的系统工程。记住黄金法则:每当你想写下${}时,先停下来,问问自己:“这个值我能完全信任吗?有没有更安全的替代方案?” 将#{}作为默认选择,将${}的使用视为需要特批的例外,是保障项目数据安全的关键第一步。
