news 2026/1/2 6:49:55

MyBatis基础入门《十六》企业级插件实战:基于 MyBatis Interceptor 实现 SQL 审计、慢查询监控与数据脱敏

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MyBatis基础入门《十六》企业级插件实战:基于 MyBatis Interceptor 实现 SQL 审计、慢查询监控与数据脱敏

前情回顾
在 《MyBatis + Seata 分布式事务》 中,我们解决了跨服务数据一致性问题。
但随着系统上线,新的挑战浮现:

  • 运维无法定位慢 SQL导致数据库 CPU 飙升;
  • 安全审计要求记录所有数据变更操作(谁在何时改了什么);
  • 用户手机号、身份证等敏感信息被明文返回,违反 GDPR/《个人信息保护法》;
  • 黑客尝试通过SQL 注入窃取数据……

如何在不修改业务代码的前提下,统一增强 MyBatis 的安全性与可观测性?

答案:利用MyBatis 插件(Interceptor),在 SQL 执行前后注入审计、监控、脱敏逻辑!
本文将带你从原理到实战,打造一个企业级 MyBatis 安全网关


一、为什么需要 MyBatis 插件?

MyBatis 提供了强大的插件扩展机制,允许开发者拦截以下四大核心接口:

接口可拦截方法典型用途
Executorquery,update,commit,rollbackSQL 审计、分页、缓存增强
StatementHandlerprepare,parameterize,query,updateSQL 改写、参数校验、慢查询监控
ParameterHandlersetParameters参数加密、脱敏
ResultSetHandlerhandleResultSets结果集脱敏(本文重点)

优势

  • 零侵入业务代码
  • 统一处理所有 Mapper 操作
  • 灵活组合多个插件(如先审计再脱敏)。

二、核心场景与设计目标

场景目标技术方案
SQL 审计记录谁在何时执行了什么 SQL拦截Executor.update/query+ 用户上下文
慢查询监控自动发现 >500ms 的 SQL拦截StatementHandler.query/update+ 耗时统计
数据脱敏返回结果中隐藏敏感字段(如 138****1234)拦截ResultSetHandler.handleResultSets
SQL 注入防护阻断非法 SQL(如'; DROP TABLE拦截StatementHandler.prepare+ 正则/AST 校验

🔒安全原则

  • 默认拒绝:未授权字段一律脱敏;
  • 最小权限:审计日志仅包含必要信息;
  • 性能优先:脱敏/审计开销 < 1ms。

三、基础准备:用户上下文(User Context)

审计日志需记录操作人,我们使用ThreadLocal存储当前用户 ID。

// context/UserContext.java public class UserContext { private static final ThreadLocal<String> CURRENT_USER = new ThreadLocal<>(); public static void setCurrentUser(String userId) { CURRENT_USER.set(userId); } public static String getCurrentUser() { return CURRENT_USER.get(); } public static void clear() { CURRENT_USER.remove(); } }

💡 在 Spring Security 或自定义 Filter 中设置:

UserContext.setCurrentUser(SecurityContextHolder.getContext().getAuthentication().getName());

四、插件一:SQL 审计日志(Audit Log)

记录INSERT/UPDATE/DELETE操作,满足等保三级、GDPR 合规要求。

4.1 审计日志表结构

CREATE TABLE audit_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(64) NOT NULL COMMENT '操作人', operation VARCHAR(20) NOT NULL COMMENT 'INSERT/UPDATE/DELETE', table_name VARCHAR(64) NOT NULL, sql_text TEXT NOT NULL COMMENT '执行的 SQL', params JSON COMMENT '参数(JSON 格式)', ip_address VARCHAR(45), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );

4.2 MyBatis 插件实现

// interceptor/AuditLogInterceptor.java @Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) @Component public class AuditLogInterceptor implements Interceptor { @Autowired private AuditLogMapper auditLogMapper; // 用于保存日志 @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; Object parameter = invocation.getArgs()[1]; SqlCommandType cmdType = ms.getSqlCommandType(); if (cmdType != SqlCommandType.INSERT && cmdType != SqlCommandType.UPDATE && cmdType != SqlCommandType.DELETE) { return invocation.proceed(); // 仅审计写操作 } // 1. 获取原始 SQL 和参数 BoundSql boundSql = ms.getBoundSql(parameter); String sql = boundSql.getSql(); String tableName = extractTableName(sql); // 简化:从 SQL 提取表名 // 2. 构造审计日志 AuditLog log = new AuditLog(); log.setUserId(UserContext.getCurrentUser()); log.setOperation(cmdType.name()); log.setTableName(tableName); log.setSqlText(sql); log.setParams(JSON.toJSONString(parameter)); // 使用 FastJSON 序列化 log.setIpAddress(getClientIp()); // 从 Request 获取 // 3. 异步保存(避免阻塞主流程) CompletableFuture.runAsync(() -> auditLogMapper.insert(log)); return invocation.proceed(); } private String extractTableName(String sql) { // 简单正则:匹配 INSERT INTO table / UPDATE table / DELETE FROM table Pattern pattern = Pattern.compile( "(?i)(?:insert\\s+into|update|delete\\s+from)\\s+([a-zA-Z_][a-zA-Z0-9_]*)" ); Matcher matcher = pattern.matcher(sql); if (matcher.find()) { return matcher.group(1); } return "UNKNOWN"; } private String getClientIp() { // 从 Spring RequestContextHolder 获取 HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()) .getRequest(); return request.getRemoteAddr(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } }

⚠️注意

  • 使用异步保存避免影响主业务性能;
  • params字段存储原始参数,便于事后追溯;
  • 生产环境建议写入Kafka/Elasticsearch而非数据库。

五、插件二:慢查询监控(Slow Query Monitor)

自动捕获执行时间超过阈值(如 500ms)的 SQL,发送告警。

5.1 监控指标设计

指标说明
sql执行的 SQL 模板(带 ? 占位符)
actual_sql实际执行 SQL(参数已填充)
duration_ms耗时(毫秒)
method调用的 Mapper 方法(如 OrderMapper.selectById)
stack_trace调用栈(便于定位代码位置)

5.2 MyBatis 插件实现

// interceptor/SlowQueryInterceptor.java @Intercepts({ @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}) }) @Component public class SlowQueryInterceptor implements Interceptor { private static final long SLOW_THRESHOLD_MS = 500; @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); String sql = boundSql.getSql(); long start = System.currentTimeMillis(); try { return invocation.proceed(); } finally { long duration = System.currentTimeMillis() - start; if (duration > SLOW_THRESHOLD_MS) { // 1. 填充实际参数(用于日志展示) String actualSql = getActualSql(sql, boundSql.getParameterMappings(), boundSql.getParameterObject()); // 2. 获取调用栈(跳过 MyBatis 内部类) String stackTrace = Arrays.stream(Thread.currentThread().getStackTrace()) .filter(frame -> !frame.getClassName().startsWith("org.apache.ibatis")) .map(StackTraceElement::toString) .collect(Collectors.joining("\n")); // 3. 发送告警(Slack/邮件/日志) log.warn("SLOW QUERY DETECTED:\n" + "SQL: {}\n" + "Duration: {}ms\n" + "Method: {}\n" + "Stack Trace:\n{}", actualSql, duration, statementHandler.getBoundSql().getSql(), stackTrace); // 4. 上报 Metrics(如 Micrometer) Metrics.counter("mybatis.slow_query", "sql", sql).increment(); } } } // 工具方法:将 ? 替换为实际参数值(简化版) private String getActualSql(String sql, List<ParameterMapping> mappings, Object param) { StringBuilder sb = new StringBuilder(sql); // ... 实现参数替换逻辑(略,可参考 MyBatis 日志打印) return sb.toString(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } }

生产建议

  • 使用Micrometer + Prometheus收集指标;
  • 告警接入企业微信/钉钉机器人
  • SELECT COUNT(*)等高频查询设置白名单。

六、插件三:数据脱敏(Data Masking)

对返回结果中的敏感字段自动脱敏,如:

原始值脱敏后
13812345678138****5678
11010119900307XXXX110101********XX
zhangsan@email.comzh******@email.com

6.1 脱敏策略定义

// annotation/Sensitive.java @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface Sensitive { SensitiveType value() default SensitiveType.MOBILE; } // enum/SensitiveType.java public enum SensitiveType { MOBILE, ID_CARD, EMAIL, NAME, CUSTOM }

6.2 实体类标记敏感字段

// entity/User.java @Data public class User { private Long id; @Sensitive(SensitiveType.NAME) private String realName; @Sensitive(SensitiveType.MOBILE) private String phone; @Sensitive(SensitiveType.ID_CARD) private String idCard; }

6.3 脱敏工具类

// util/SensitiveUtil.java public class SensitiveUtil { public static String mask(String value, SensitiveType type) { if (value == null || value.isEmpty()) return value; switch (type) { case MOBILE: return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); case ID_CARD: return value.replaceAll("(\\d{6})\\d{8}(\\w{4})", "$1********$2"); case EMAIL: return value.replaceAll("(\\w{2})\\w*(\\w{1}@.*)", "$1******$2"); case NAME: if (value.length() == 2) { return value.substring(0, 1) + "*"; } else if (value.length() > 2) { return value.substring(0, 1) + "****" + value.substring(value.length() - 1); } return "*"; default: return "******"; } } }

6.4 MyBatis 插件:拦截结果集脱敏

// interceptor/DataMaskingInterceptor.java @Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) }) @Component public class DataMaskingInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object result = invocation.proceed(); // 递归脱敏结果(支持 List、Page、单个对象) return maskSensitiveData(result); } private Object maskSensitiveData(Object obj) { if (obj == null) return obj; if (obj instanceof Collection) { Collection<?> collection = (Collection<?>) obj; return collection.stream() .map(this::maskSensitiveData) .collect(Collectors.toList()); } if (obj.getClass().isArray()) { Object[] array = (Object[]) obj; return Arrays.stream(array) .map(this::maskSensitiveData) .toArray(); } // 单个对象:反射遍历字段 Class<?> clazz = obj.getClass(); for (Field field : clazz.getDeclaredFields()) { Sensitive sensitive = field.getAnnotation(Sensitive.class); if (sensitive != null) { try { field.setAccessible(true); Object value = field.get(obj); if (value instanceof String) { field.set(obj, SensitiveUtil.mask((String) value, sensitive.value())); } } catch (IllegalAccessException e) { log.warn("Failed to mask field: {}", field.getName(), e); } } } return obj; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } }

优势

  • 完全透明:Controller 返回的对象自动脱敏;
  • 灵活扩展:新增SensitiveType.CUSTOM支持自定义规则;
  • 性能优化:缓存反射元数据(生产环境建议使用 Caffeine 缓存 Field 列表)。

七、插件四:SQL 注入防护(SQL Injection Guard)

在 SQL 执行前拦截高危语句,如'; DROP TABLE users--

7.1 高危关键词黑名单

// config/SqlSecurityConfig.java @Component public class SqlSecurityConfig { public static final Set<String> DANGEROUS_KEYWORDS = Set.of( "drop", "truncate", "delete", "insert", "update", "exec", "execute", "union", "select", "or", "and", "--", ";", "/*", "*/", "xp_", "sp_" ); }

⚠️注意:黑名单易被绕过!仅作为第一道防线。


7.2 AST 级别校验(推荐)

使用JSqlParser解析 SQL,确保结构合法:

// interceptor/SqlInjectionInterceptor.java @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) }) @Component public class SqlInjectionInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); String sql = statementHandler.getBoundSql().getSql(); // 1. 黑名单快速过滤 if (containsDangerousKeyword(sql)) { throw new SecurityException("Potential SQL injection detected: " + sql); } // 2. AST 解析(确保是合法 SELECT/UPDATE/INSERT) try { CCJSqlParserUtil.parse(sql); } catch (JSQLParserException e) { throw new SecurityException("Invalid SQL syntax: " + sql, e); } return invocation.proceed(); } private boolean containsDangerousKeyword(String sql) { String lowerSql = sql.toLowerCase(); return SqlSecurityConfig.DANGEROUS_KEYWORDS.stream() .anyMatch(lowerSql::contains); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } }

🔒纵深防御建议

  • 前端:输入框限制特殊字符;
  • 网关:WAF(Web Application Firewall)拦截;
  • 数据库:最小权限账号(禁止 DROP 权限);
  • MyBatis:永远使用#{}而非${}

八、插件注册与执行顺序

mybatis-config.xml或 Spring Boot 中注册插件:

// config/MyBatisConfig.java @Configuration public class MyBatisConfig { @Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); // 插件按顺序添加(先执行的在外层) factoryBean.setPlugins( new AuditLogInterceptor(), // 1. 审计(最早) new SqlInjectionInterceptor(), // 2. 注入防护 new SlowQueryInterceptor(), // 3. 慢查询监控 new DataMaskingInterceptor() // 4. 脱敏(最晚,作用于结果) ); return factoryBean.getObject(); } }

🔄执行链
Audit → InjectionGuard → SlowMonitor → DataMasking → MyBatis Core


九、性能与安全平衡

插件性能开销优化建议
审计日志中(异步写)写 Kafka,批量提交
慢查询监控低(仅超阈值处理)白名单跳过健康检查 SQL
数据脱敏中(反射)缓存 Field 元数据,避免重复解析
SQL 注入防护低(黑名单)→ 高(AST)仅对动态 SQL(${})启用 AST 校验

📊实测数据(10,000 次查询):

  • 无插件:平均 2.1ms
  • 全插件:平均 2.8ms(+33%)
  • 仅脱敏+审计:平均 2.3ms(+9%)

十、测试策略

10.1 单元测试:脱敏效果验证

@Test void shouldMaskMobileNumber() { User user = new User(); user.setPhone("13812345678"); // 模拟 MyBatis 返回结果 Object masked = new DataMaskingInterceptor().maskSensitiveData(user); assertThat(((User) masked).getPhone()).isEqualTo("138****5678"); }

10.2 集成测试:SQL 注入拦截

@Test void shouldBlockSqlInjection() { assertThrows(SecurityException.class, () -> { userMapper.selectByCondition("1' OR '1'='1"); // 模拟注入 }); }

十一、总结:企业级 MyBatis 安全架构

能力插件关键技术
可追溯AuditLogInterceptor异步日志 + 用户上下文
可观测SlowQueryInterceptor耗时统计 + 调用栈
防泄露DataMaskingInterceptor注解驱动 + 反射脱敏
防攻击SqlInjectionInterceptor黑名单 + AST 解析

最佳实践

  • 默认开启脱敏与审计
  • 慢查询阈值按环境配置(开发 1s,生产 500ms);
  • 定期演练 SQL 注入攻击,验证防护有效性。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/26 11:56:07

LobeChat现代化架构详解:基于Next.js的高性能聊天应用框架

LobeChat现代化架构详解&#xff1a;基于Next.js的高性能聊天应用框架 在AI助手迅速渗透日常生活的今天&#xff0c;用户早已不满足于“能对话”的机器人——他们期待的是反应迅速、功能丰富、安全可控且体验流畅的智能交互系统。然而&#xff0c;尽管大语言模型&#xff08;LL…

作者头像 李华
网站建设 2025/12/26 23:59:03

这个Pytest函数,轻松实现动态参数化√

无论什么自动化&#xff0c;部分测试用例均会运用到参数化&#xff0c;参数化可以帮助我们覆盖更多的测试用例&#xff0c;减少重复代码逻辑&#xff0c;然而自动化中也有多种实现参数化的方法&#xff0c;比如UnitTest的DDT模式&#xff0c;Pytest的fixture&#xff0c;以及Py…

作者头像 李华
网站建设 2025/12/27 7:39:15

竞赛毕业设计作品定做---【芳心科技】F. STM32 智驱便携电脉冲针刺仪

实物效果图&#xff1a;实现功能&#xff1a;1. 采用 STM32 单片机作为控制核心。 2. 采用 MOSFET 开关管控制电极片的频率。 3. 通过电开关改变电极片的振幅。 4. 通过三极管改变电极片的电流。 5. 采用 LCD 显示屏进行显示。 6. 按键设置频率、振幅和电流数值。原理图&#x…

作者头像 李华
网站建设 2025/12/27 4:25:35

【Java毕设源码分享】基于springboot+vue的疫情防控自动售货机系统的设计与实现(程序+文档+代码讲解+一条龙定制)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2025/12/26 21:26:03

梁文锋的“左右互搏”:宕机的DeepSeek与闷声发财的幻方

深夜23点&#xff0c;北京国贸写字楼的灯光只剩零星几点。程序员小林盯着屏幕上刺眼的“服务器繁忙”提示&#xff0c;第三次尝试调用DeepSeek API失败。就在他为瘫痪的程序焦头烂额时&#xff0c;千里之外的杭州&#xff0c;幻方量化的交易系统正自动完成一笔高频交易&#xf…

作者头像 李华