${}是字符串直接替换,存在SQL注入风险;#{}是预编译占位符,通过PreparedStatement绑定参数,安全防注入,适用于值传递,而${}仅限白名单校验后的SQL片段(如表名、排序字段)。

不能安全拼接动态SQL——只要拼接发生,参数化就失效。所谓“安全拼接”是伪命题,真正可行的只有「结构白名单校验 + 值参数化绑定」双轨并行。
MyBatis 的 ${} 和 #{} 到底差在哪
`${}` 是字符串替换,运行时直接把变量值塞进 SQL 字符串里,数据库收到的就是完整可执行语句;`#{}` 是预编译占位符,JDBC 驱动会把值作为独立参数传给 PreparedStatement,不参与语法解析。
- 错误示例:
—— 表名、ID 全部裸奔 - 正确做法:表名必须从
Map.of("user", "t_user", "order", "t_order")中取,ID 必须走#{id} - 别信“只拼字段名就没事”:传入
status; DROP TABLE config--,Oracle 或 MySQL 都可能多语句执行(尤其开启allowMultiQueries=true时)
JdbcTemplate 动态表名/排序字段怎么处理
JdbcTemplate 完全不支持对表名、列名、ORDER BY 子句做参数化,它的 ? 只绑定值。任何字符串拼接都绕过 PreparedStatement 防护。
- 必须提前定义白名单:
private static final SetALLOWED_TABLES = Set.of("users", "products", "orders"); - 校验逻辑要严格:
if (!ALLOWED_TABLES.contains(tableName)) { throw new IllegalArgumentException("Invalid table name"); } - 排序字段同理:允许的
sortField只能是"id"、"created_at"、"status"等固定值,且需配合sortOrder白名单(仅"ASC"/"DESC") - 别用正则模糊匹配,比如
sortField.matches("[a-zA-Z_]+")—— 攻击者可填id ASC, (SELECT password FROM users LIMIT 1)
IN 查询和动态条件组合的替代方案
原生 JdbcTemplate 不支持变长 IN 列表,硬拼 WHERE id IN (?, ?, ?) 会导致参数数量不匹配;而用 apply("id IN (" + idsStr + ")") 直接破防。
- 首选
NamedParameterJdbcTemplate:SELECT * FROM users WHERE status = :status AND id IN (:ids),再用MapSqlParameterSource("ids", List.of(1L, 2L, 3L))传参 - 动态 WHERE 条件不要拼 SQL,改用模板 + 参数列表:
String sql = "SELECT * FROM users WHERE 1=1" + (status != null ? " AND status = ?" : "") + (minAge > 0 ? " AND age > ?" : "");,然后按顺序传参 - 避免在 SQL 字符串里写
"%" + keyword + "%"—— 应该用CONCAT('%', ?, '%')或数据库函数封装
为什么拦截器和正则过滤不是解决方案
SQL 注入载荷可以藏在 JSON body、multipart 表单、Header、甚至 Base64 编码字段里,HandlerInterceptor 拿不到原始请求体,Filter 又容易误杀(比如用户昵称含 or 就被拦)。
-
SqlInjectInterceptor这类 MyBatis 拦截器只能扫出显式关键字,对1'='1、admin' --等变形无效 - 正则模式如
(?i)union.*select会被注释符、URL 编码、大小写混用绕过 - 真正要做的,是在 DAO 层拒绝一切未白名单校验的结构变量——哪怕它来自内部服务调用,也不能放松校验
最常被忽略的一点:白名单必须覆盖所有动态位置,包括 CTE 别名、临时表名、自定义函数名。只要某处 SQL 结构由运行时变量决定,就必须有对应校验逻辑,而不是靠“这里应该不会被用户控制”来赌运气。











