
01 凌晨3点的惊魂时刻
你有没有想过,职业生涯的终结,可能只需要 0.1 秒?
上周五,某互联网大厂(化名)的生产环境发生了一起惨案。一名入职不到半年的后端开发小哥,在处理一个看似简单的“清理过期日志”需求时,手指在回车键上轻轻一敲。
那一瞬间,整个运维群炸了。
CPU 飙升,IO 报警,随后是死一般的寂静——数据库连接池空了。
这不仅是技术故障,这是刑事案件的预演。
02 凶手复现:那行致命的代码
只要你用 Java 写代码,用 MyBatis、TkMyBatis 或 MyBatis-Plus,这个坑你可能正踩在脚下。

那个小哥的需求很简单:删除 t_log 表中 30 天前的日志。
他写下的 Mapper 是这样的:
<delete id="deleteExpiredLogs">
DELETE FROM t_log
<if test="expireDate != null">
WHERE create_time #{expireDate}
</if>
</delete>
看出问题了吗?
在调用这个方法时,由于上游逻辑的一个 Bug,传入的 expireDate 参数是 null。
MyBatis 的动态 SQL 极其听话,既然参数是 null,<if> 标签不成立。于是,执行的 SQL 变成了:
DELETE FROM t_log
没有 WHERE 条件。全表删除。
500 万条用户行为日志,瞬间灰飞烟灭。如果这是订单表,或者是用户资产表,你现在的工位可能已经在派出所了。
03 为什么你的测试环境没测出来?
很多时候,这种 Bug 具有极强的隐蔽性:
- 测试数据量小:测试库只有几条数据,删了也就删了,毫秒级响应,没有任何感知。
- 参数覆盖不全:测试用例通常只覆盖了“正常传参”的情况,忽略了“参数丢失”的边界场景。
- 侥幸心理:以为前端校验了,后端就不用防守了。
04 绝地求生:三道防线保住你的饭碗
如果不希望这种事发生在自己身上,请立刻检查你的项目。我们不需要靠“小心”,我们要靠“机制”。

第一道防线:配置 MyBatis-Plus 攻击拦截器(推荐)
如果你使用的是 MyBatis-Plus,官方其实早就提供了一个神级插件——BlockAttackInnerInterceptor(防全表更新/删除插件)。
这就是今天的「保命源码」:
创建一个配置类,加上这个拦截器:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加防全表更新/删除拦截器
// 核心逻辑:分析SQL,如果发现 DELETE/UPDATE 且没有 WHERE 条件,直接抛异常!
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
}
效果演示:
当你再次执行那行致命代码时,控制台会抛出异常:
com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of full table deletion
它会直接阻断 SQL 的发送,救你一命!
没有 MP,我们手写一道“防爆盾”
使用的是原生 MyBatis 或者 TkMapper,没有 Plus 的那个插件。既然官方没给,我们就自己造一个!这才是资深开发该有的硬核素质。
我们需要借助一个工具:JSqlParser。它能把 SQL 语句拆解成语法树,让我们看清 SQL 到底想干什么。
第一步:引入依赖
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>4.6</version>
</dependency>
第二步:复制这段价值连城的拦截器代码
这是一个通用的 MyBatis 拦截器,同时适用于原生 MyBatis 和 TkMapper。它会拦截所有的 Update 和 Delete 操作,强制检查是否存在 WHERE 条件。
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Component
public class SqlSafetyInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
// 只拦截写操作 (Update/Delete)
if (ms.getSqlCommandType() != SqlCommandType.UPDATE && ms.getSqlCommandType() != SqlCommandType.DELETE) {
return invocation.proceed();
}
// 1. 获取绑定的SQL
BoundSql boundSql = ms.getBoundSql(parameter);
String sql = boundSql.getSql();
// 2. 解析SQL (关键步骤)
try {
Statement statement = CCJSqlParserUtil.parse(sql);
// 3. 针对删除操作的防御
if (statement instanceof Delete) {
Delete delete = (Delete) statement;
if (delete.getWhere() == null) {
throw new SQLException("危险操作拦截:禁止全表删除!请检查代码!SQL: " + sql);
}
}
// 4. 针对更新操作的防御
else if (statement instanceof Update) {
Update update = (Update) statement;
if (update.getWhere() == null) {
throw new SQLException("危险操作拦截:禁止全表更新!请检查代码!SQL: " + sql);
}
}
} catch (JSQLParserException e) {
// 解析失败时,建议根据策略决定是放行还是阻断,这里为了安全选择打印日志
System.err.println("SQL解析失败,请人工复查: " + sql);
}
return invocation.proceed();
}
}
TkMapper 特别警示:
如果你用的是 TkMapper,有一个极度危险的坑:deleteByExample(Example example)。
如果传入的 example 是 null,或者通过 new Example(Class) 创建后没有添加任何 criteria,生成的 SQL 就是全表删除!
上述的拦截器完美防御了这个场景,因为无论 TkMapper 怎么生成 SQL,最终都会经过这个拦截器。
第三步:注册拦截器
如果你是 Spring Boot 项目,直接在类上加 @Component 即可(如上代码所示)。
如果是老旧的 XML 配置项目,请在 mybatis-config.xml 中添加:
<plugins>
<plugin interceptor="com.yourpackage.interceptor.SqlSafetyInterceptor"/>
</plugins>
- 痛点打击: “MyBatis-Plus 用户虽然爽,但还在坚持用 TkMapper 和原生 MyBatis 的兄弟们,你们才是护城河的基石。但正因为缺少现成的轮子,我们裸奔的风险更高。”
- 技术优越感: “只会引依赖调 API 叫调包侠,能手写
Interceptor操纵 SQL 解析树的,才是架构师苗子。今天这段代码,面试时拿出来讲,绝对是加分项。”
第二道防线:SQL 里的 1=1 陷阱
很多老系统喜欢写 WHERE 1=1。请注意,BlockAttackInnerInterceptor 也能识别大部分这种绕过行为,但尽量规范代码:
错误示范(极其危险):
DELETE FROM t_user WHERE 1=1
<if test="id != null">
AND id = #{id}
</if>
如果 id 为空,SQL 变成 DELETE FROM t_user WHERE 1=1,依然是全表删除。
正确姿势:
使用 <where> 标签,它会自动处理第一个 AND 问题,且如果标签内无内容,不会生成 WHERE 关键字(需配合拦截器使用)。
第三道防线:数据库层面的“保险丝”
如果你是 DBA 或者有权限管理数据库,可以在 MySQL 服务端设置 sql_safe_updates 参数。
SET GLOBAL sql_safe_updates = 1;
开启后,如果 UPDATE 或 DELETE 语句中没有使用索引键作为 WHERE 条件,或者没有 LIMIT 子句,MySQL 会直接拒绝执行。
05 写在最后
技术没有黑魔法,只有敬畏心。
哪怕你写了十年的 Java,一次手抖也足以毁掉一切。今天回去,赶紧加上那个拦截器。
转发给你的开发群,这可能是一篇能救命的文章。
Q.E.D.


