防止SQL注入的核心在于正确使用MyBatis中的#{}和${}占位符。#{}会进行预编译处理,将参数安全地传递给SQL,从而有效防止SQL注入;而${}则是直接的字符串替换,存在注入风险。因此,在大多数情况下,应优先使用#{},仅在动态表名、列名等特定场景下谨慎使用${},并确保参数可控。

#{}与${}的本质区别:预编译与字符串拼接

#{}在MyBatis中被称为参数占位符,它会在SQL执行前进行预编译处理。MyBatis会将#{}替换为?,然后通过PreparedStatement将参数安全地设置到SQL中,这个过程能对特殊字符进行转义,从而杜绝SQL注入。例如,用户输入"' OR '1'='1"时,使用#{}会将其视为普通字符串,而不是可执行的SQL代码。

SELECT * FROM users WHERE username = #{name}

实际执行的SQL类似于:

SELECT * FROM users WHERE username = ?

参数"name"的值会被安全地绑定到问号处。

${}则是字符串替换占位符,MyBatis会直接将参数值替换到SQL语句中,相当于字符串拼接。如果参数来自用户输入且未经验证,恶意SQL代码就会被直接执行。

SELECT * FROM users WHERE username = '${name}'

如果"name"的值为"admin' OR '1'='1",拼接后的SQL变为:

SELECT * FROM users WHERE username = 'admin' OR '1'='1'

这将导致查询出所有用户数据,构成SQL注入。

为什么必须优先使用#{}?

从安全角度,预编译是防止SQL注入最有效的手段之一。数据库系统会对预编译SQL的模板进行解析和优化,后续传入的参数无论包含什么内容,都只被视为数据,不会改变SQL语义。这从根本上切断了注入途径。从性能角度,预编译语句可以被数据库缓存和重用,提高执行效率。因此,对于所有用户输入或不可信数据,如WHERE条件值、INSERT/UPDATE的数据,必须使用#{}。

${}的合法使用场景与严格规范

${}并非完全禁用,但其使用必须严格限制在参数值并非来自用户直接输入、且内容完全可控的场景。主要场景包括动态表名和列名,例如在分表场景下,表名需要根据日期动态生成。

SELECT * FROM order_${yearMonth}

这里的"yearMonth"必须是后端系统生成的、格式受控的值(如"202310"),绝不能由前端用户任意传递。使用规范包括:

(1) 参数值必须在服务端代码中生成或经过严格白名单验证;

(2) 绝不将用户输入直接用于${};

(3) 必要时对参数进行过滤,只允许特定字符集(如数字、字母)。

常见错误用法与注入案例剖析

一个典型的错误是在ORDER BY子句中误用${}进行动态排序。开发者可能为了让用户选择排序字段而写下这样的代码:

SELECT * FROM products ORDER BY ${sortField}

如果攻击者将"sortField"参数设置为"price; DROP TABLE products --",在某些数据库配置下可能导致灾难性后果。正确的做法是使用#{},但ORDER BY后接字段名不能使用预编译的?。此时,应在后端将用户输入值(如"price")与预定义的字段白名单(如["price", "name"])进行比对,只有匹配时才使用${sortField},或者更安全地在业务层构造完整的SQL片段。

另一个常见错误是模糊查询中的误用。为了实现"LIKE '%keyword%'",有人会写成:

SELECT * FROM items WHERE name LIKE '%${keyword}%'

这同样是高风险操作。应使用#{}并结合SQL的CONCAT函数(或数据库等效函数):

SELECT * FROM items WHERE name LIKE CONCAT('%', #{keyword}, '%')

进阶防御:结合其他安全实践

除了正确使用占位符,还需建立纵深防御体系。首先,实施最小权限原则,数据库操作账户不应拥有DBA或DROP TABLE等高危权限。其次,对所有输入进行严格的验证和过滤,包括类型、长度、格式等,即便使用#{},验证也能防止业务逻辑错误。第三,在DAO层或ORM框架外,可以考虑使用Web应用防火墙规则来检测和拦截可疑的SQL注入模式。第四,定期进行代码审计和安全扫描,重点关注所有使用${}的地方。最后,在MyBatis配置中,可以结合使用其内置的"@Param"注解和动态SQL标签(如"<if>"、"<choose>")来安全地构建复杂查询,避免手动拼接。

总结与最佳实践清单

防止SQL注入是Web开发的底线要求。在MyBatis中,请遵循以下清单:

(1) 默认使用#{},将其作为首选;

(2) 使用${}仅限于动态表名/列名等必要场景,且参数必须服务端可控;

(3) 禁止将任何用户请求参数直接用于${};

(4) 对动态内容建立白名单验证机制;

(5) 将MyBatis的日志级别设为DEBUG,定期检查实际执行的SQL语句,排查可疑拼接;

(6) 将安全编码规范纳入团队培训和代码审查流程。通过工具与意识的结合,才能构建真正坚固的应用防线。