动态列名不能直接拼接,因SQL解析阶段需确定列名,PreparedStatement的?仅支持值而非标识符,字符串拼接易致SQL注入;应使用不可变Map白名单映射逻辑名到真实列名,并校验排序方向。

动态列名为什么不能直接拼接
因为 ORDER BY、GROUP BY、SELECT 中的列名在 SQL 解析阶段就必须确定,JDBC 的 PreparedStatement 占位符 ? 只能用于**值**,不能用于标识符(如列名、表名、函数名)。直接字符串拼接 column + " ASC" 就等于把用户输入裸奔进 SQL,只要传入 id; DROP TABLE users-- 这类 payload,就可能触发注入。
用 Map 做字段白名单映射的实际写法
核心是把前端/外部传入的“逻辑字段名”(比如 "userName")映射到数据库真实列名(比如 "user_name"),且只允许映射表中预定义的键。不是靠正则过滤,而是靠查表失败即拒绝。
示例(Java):
private static final Map<String, String> SORT_COLUMN_WHITELIST = Map.of(
"userName", "user_name",
"createdAt", "created_at",
"status", "status_code"
);
public String getSortClause(String sortField, String sortOrder) {
String realColumn = SORT_COLUMN_WHITELIST.get(sortField);
if (realColumn == null) {
throw new IllegalArgumentException("Invalid sort field: " + sortField);
}
if (!"ASC".equalsIgnoreCase(sortOrder) && !"DESC".equalsIgnoreCase(sortOrder)) {
throw new IllegalArgumentException("Invalid sort order: " + sortOrder);
}
return realColumn + " " + sortOrder.toUpperCase();
}
- Map 必须是不可变的(如
Map.of()或Collections.unmodifiableMap()),防止运行时被篡改 - 键(
"userName")是接口约定的字段名,值("user_name")才是 DB 实际列名,两者解耦 - 不要用
containsKey()再 get —— 一次查找更安全,避免竞态(虽然极小概率)
ORDER BY 场景下多个字段排序的白名单处理
当支持多字段排序(如 sort=userName,createdAt&order=ASC,DESC)时,不能简单 split 后逐个查 Map,必须校验整个组合是否合法——否则攻击者可能绕过单字段检查,拼出非法语句。
推荐做法:
- 拆分后对每个字段单独走白名单映射,任一失败即拒
- 排序方向也需单独校验,不允许空值或非 ASC/DESC
- 最终拼接前,用
String.join(", ", clauses)而非字符串累加,避免意外空格或逗号残留 - 如果业务允许固定组合(如只允许
"userName DESC, createdAt ASC"),可直接用枚举或预定义字符串列表比对,更严格
MyBatis 中怎么安全集成这个白名单
MyBatis 的 <bind></bind> 和 OGNL 表达式不解决注入问题,${} 是危险的,#{} 又不支持列名。所以白名单校验必须放在 DAO 层或 Service 层,**不能依赖 MyBatis 自身过滤**。
正确姿势:
- 在调用
mapper.listUsers(...)前,先调用getSortClause(sortField, sortOrder)获取安全 SQL 片段 - 把这个结果作为参数传给 MyBatis,用
${sortClause}插入 —— 此时已确保是白名单内的值,${} 才可控 - 切勿把原始
sortField直接传进 XML 里做${sortField},那是裸奔 - XML 中应写作:
ORDER BY ${safeSortClause},而safeSortClause是经 Java 层校验并拼好的字符串
白名单本身最好从配置中心或 classpath 静态文件加载,而不是硬编码在方法里——但加载后仍需转为不可变 Map,运行时不再变更。

















