在后台管理系统中,列表查询几乎都离不开动态排序功能。MyBatis-Plus提供了非常便捷的Wrapper条件构造器,让开发者可以用代码方式拼装SQL。但如果排序字段来自前端请求且未加限制,就极易产生SQL注入漏洞。本文围绕如何利用白名单机制安全地实现动态排序展开说明。

一、动态排序为何会产生SQL注入
MyBatis-Plus的orderBy系列方法在底层会将字段名直接拼接进SQL语句。很多开发者为了省事,会把前端传来的sortField参数直接传入,例如orderBy(true, isAsc, sortField)。由于MyBatis-Plus不会对字段名做语法校验,攻击者完全可以传入类似id; drop table user;这样的内容,造成极其严重的后果。
更隐蔽的攻击方式是利用排序字段注入子查询或函数,比如传入(case when (select count(*) from admin)=1 then id else name end)。这类 payload 在日志里看起来只是一个字段名,却能探测数据库结构。因此,只要排序字段是外部输入,就必须视为不可信数据。
二、白名单机制的设计思路
白名单机制的本质是:系统预先声明哪些字段允许被排序,运行時只接受白名单内的字段,其余一律拒绝。这与黑名单不同,黑名单试图列举危险字符,永远防不住新变种;白名单则缩小可信范围,安全性更高。
在MyBatis-Plus项目中,白名单可以用枚举、常量类或数据库配置表来维护。推荐将白名单与实体类的真实列名绑定,避免拼写错误。同时,排序方向(升序或降序)也应限制为固定值,不能由用户自由输入任意字符串。
三、基于枚举的白名单实现示例
下面给出一个使用Java枚举维护白名单字段,并在Service层做校验的完整示例。该方案不依赖任何额外组件,适合大多数Spring Boot项目。
// 定义允许排序的字段白名单
public enum AllowedSortField {
ID("id"),
USERNAME("username"),
CREATE_TIME("create_time");
private final String column;
AllowedSortField(String column) {
this.column = column;
}
public String getColumn() {
return column;
}
// 根据前端传入的名称匹配,找不到返回null
public static String from(String fieldName) {
if (fieldName == null) {
return null;
}
for (AllowedSortField f : values()) {
if (f.name().equalsIgnoreCase(fieldName) || f.column.equals(fieldName)) {
return f.column;
}
}
return null;
}
}
// Service层使用方式
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
public List<User> listBySort(String field, String order) {
// 校验字段是否在白名单
String safeField = AllowedSortField.from(field);
if (safeField == null) {
// 不在白名单,使用默认排序或抛异常
safeField = "id";
}
// 校验排序方向
boolean isAsc = !"desc".equalsIgnoreCase(order);
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.orderBy(true, isAsc, safeField);
return list(wrapper);
}
}
上述代码中,AllowedSortField.from方法负责将任意前端参数转换为安全的列名,转换失败就降级为默认字段。这样即便攻击者传入恶意字符串,也只会落到id上,不会拼进SQL造成注入。
这种写法的优点是逻辑清晰、易于单测。如果以后表结构变更,只需修改枚举,不会遗漏校验点。相比在每一个Controller里写判断,集中到Service或独立校验工具类更利于维护。
四、与MyBatis-Plus分页插件结合
实际项目中动态排序常和分页一起出现。MyBatis-Plus的分页对象Page支持直接设置排序,但原理同上,字段仍需白名单过滤。
public IPage<User> pageBySort(long current, long size, String field, String order) {
String safeField = AllowedSortField.from(field);
if (safeField == null) {
safeField = "id";
}
boolean isAsc = !"desc".equalsIgnoreCase(order);
Page<User> page = new Page<>(current, size);
page.addOrder(new OrderItem(safeField, isAsc));
return page(page);
}
使用OrderItem时,字段名同样不能信任。上面代码在构造OrderItem之前已经完成白名单转换,保证进入分页插件的只能是合法列。
如果团队使用了代码生成器,也可以把白名单枚举一并生成,从源头保证实体字段和排序白名单一致,减少人工同步成本。
五、常见误区与补充建议
有的开发者认为只要用SqlInjector或MyBatis-Plus的防注入插件就万事大吉,其实那些主要拦截的是恶意表达式和危险关键字,对排序字段名这种“看起来合法”的输入无能为力。白名单是目前最稳妥的做法。
另外,不要在前端用下拉框隐藏真实字段名就以为安全,攻击者可以直接抓包改参数。所有校验必须在服务端完成。若系统排序规则复杂,可将白名单放入配置中心,支持热更新,无需发版即可调整可信字段。
| 方案 | 安全性 | 维护成本 | 适用场景 |
|---|---|---|---|
| 直接拼接前端字段 | 极低 | 低 | 内部无外网系统(仍不推荐) |
| 黑名单过滤字符 | 中低 | 中 | 遗留系统快速修补 |
| 枚举白名单 | 高 | 低 | 绝大多数业务系统 |
| 配置表白名单 | 高 | 中 | 字段多且常变的大型系统 |
通过白名单机制限制排序字段,可以在保留动态排序能力的同时,彻底封堵MyBatis-Plus动态排序带来的SQL注入风险。建议在新项目搭建时就将排序字段校验作为规范固定下来。
MyBatis-PlusSQL注入白名单机制修改时间:2026-08-10 04:54:30