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 具有极强的隐蔽性:

  1. 测试数据量小:测试库只有几条数据,删了也就删了,毫秒级响应,没有任何感知。
  2. 参数覆盖不全:测试用例通常只覆盖了“正常传参”的情况,忽略了“参数丢失”的边界场景。
  3. 侥幸心理:以为前端校验了,后端就不用防守了。

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)
如果传入的 examplenull,或者通过 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.


寻门而入,破门而出