数据脱敏(Data Masking)是指对敏感信息(如身份证号、手机号、银行卡号等)进行变形、屏蔽或替换,使其在非生产环境或非授权场景下不可识别,同时保留数据的其他可用性,例如:日志记录,避免敏感信息写入日志文件;接口返回时前端展示时隐藏部分数据(如手机号显示为 138****1234)。MyBatis 插件是实现数据脱敏的一种有效且侵入性较低的方式,利用 MyBatis 提供的拦截器(Interceptor)机制,在 SQL 执行的不同阶段对数据进行处理。通过这种方式,可以在数据从数据库读取出来或写入数据库之前,对其进行脱敏或恢复操作,而无需修改大量的业务代码。
MyBatis 插件实现数据脱敏的核心原理
MyBatis 允许你拦截四大对象:
Executor: 负责底层 SQL 的执行,包括事务管理和缓存。 
StatementHandler: 负责处理 SQL 语句的生成和参数的设置。 
ParameterHandler: 负责处理 SQL 参数的设置。 
ResultSetHandler: 负责处理结果集的封装,将数据库返回的数据映射到 Java 对象。 
实现数据脱敏通常会用到 ParameterHandler(用于写入时脱敏)和 ResultSetHandler(用于读取时脱敏)。
实现步骤
下面我们将通过一个简单的示例来演示如何使用 MyBatis 插件实现数据脱敏。假设我们有一个 User 表,其中包含敏感字段 phone(手机号)和 id_card(身份证号),我们需要在读取时对其进行部分遮蔽。
1. 定义脱敏注解
首先,我们定义一个自定义注解,用于标识需要脱敏的字段及其脱敏策略。
1 2 3 4 5 6 7 8
   | import java.lang.annotation.*;
  @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SensitiveData {     SensitiveStrategy strategy(); }
   | 
 
定义脱敏策略枚举:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   | public enum SensitiveStrategy {     
 
      NONE,     
 
      PHONE,     
 
      ID_CARD,     
 
      CUSTOM; }
  | 
 
2. 定义脱敏工具类
这个工具类包含具体的脱敏逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
   | public class SensitiveUtils {
      
 
      public static String hidePhone(String phone) {         if (phone == null || phone.length() != 11) {             return phone;         }         return phone.replaceAll("(\d{3})\d{4}(\d{4})", "$1****$2");     }
      
 
      public static String hideIdCard(String idCard) {         if (idCard == null || (idCard.length() != 15 && idCard.length() != 18)) {             return idCard;         }         return idCard.replaceAll("(\d{6})\d{8}(\w{4})", "$1********$2");     }
           public static String customMask(String data) {         if (data == null || data.length() < 2) {             return data;         }                  return data.charAt(0) + "*" + data.substring(2);     }
      
 
      public static String mask(String data, SensitiveStrategy strategy) {         if (data == null || data.isEmpty()) {             return data;         }         switch (strategy) {             case PHONE:                 return hidePhone(data);             case ID_CARD:                 return hideIdCard(data);             case CUSTOM:                 return customMask(data);             case NONE:             default:                 return data;         }     } }
  | 
 
3. 创建数据实体类
在实体类的敏感字段上加上我们定义的 @SensitiveData 注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
   | import com.example.plugin.annotation.SensitiveData; import com.example.plugin.annotation.SensitiveStrategy;
  public class User {     private Long id;     private String name;
      @SensitiveData(strategy = SensitiveStrategy.PHONE)     private String phone;
      @SensitiveData(strategy = SensitiveStrategy.ID_CARD)     private String idCard;
           public Long getId() { return id; }     public void setId(Long id) { this.id = id; }     public String getName() { return name; }     public void setName(String name) { this.name = name; }     public String getPhone() { return phone; }     public void setPhone(String phone) { this.phone = phone; }     public String getIdCard() { return idCard; }     public void setIdCard(String idCard) { this.idCard = idCard; }
      @Override     public String toString() {         return "User{" +                "id=" + id +                ", name='" + name + ''' +                ", phone='" + phone + ''' +                ", idCard='" + idCard + ''' +                '}';     } }
   | 
 
4. 编写 MyBatis 脱敏插件
实现 Interceptor 接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
   | import com.example.plugin.annotation.SensitiveData; import com.example.plugin.annotation.SensitiveStrategy; import com.example.plugin.util.SensitiveUtils; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.plugin.*; import java.sql.Statement; import java.lang.reflect.Field; import java.util.*;
 
  @Intercepts({     @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) }) public class DataMaskingInterceptor implements Interceptor {
      @Override     public Object intercept(Invocation invocation) throws Throwable {                  Object result = invocation.proceed();
                   if (result == null) {             return null;         }
          if (result instanceof ArrayList) {             ArrayList<?> list = (ArrayList<?>) result;             if (!list.isEmpty()) {                                  for (Object o : list) {                     maskSensitiveFields(o);                 }             }         } else {                          maskSensitiveFields(result);         }         return result;     }
      private void maskSensitiveFields(Object obj) throws IllegalAccessException {         if (obj == null) {             return;         }
          Class<?> clazz = obj.getClass();                  List<Field> fields = new ArrayList<>();         while (clazz != null) {             fields.addAll(Arrays.asList(clazz.getDeclaredFields()));             clazz = clazz.getSuperclass();         }
          for (Field field : fields) {                          if (field.isAnnotationPresent(SensitiveData.class)) {                 SensitiveData sensitiveData = field.getAnnotation(SensitiveData.class);                 SensitiveStrategy strategy = sensitiveData.strategy();
                                   field.setAccessible(true);                 Object value = field.get(obj);
                  if (value instanceof String) {                     String maskedValue = SensitiveUtils.mask((String) value, strategy);                     field.set(obj, maskedValue);                 }                              }         }     }
      @Override     public Object plugin(Object target) {                  return Plugin.wrap(target, this);     }
      @Override     public void setProperties(Properties properties) {              } }
   | 
 
5. 配置 MyBatis 插件
在 SpringBoot 当中注册 MyBatis 插件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   | @Configuration public class MyBatisConfig {
      @Bean     public DataMaskingInterceptor dataMaskingInterceptor() {         return new DataMaskingInterceptor();     }
      @Bean     public ConfigurationCustomizer mybatisConfigurationCustomizer() {         return configuration -> {                          configuration.addInterceptor(dataMaskingInterceptor());         };     } }
 
   | 
 
6. 编写 Mapper 接口和 XML
1 2 3 4 5 6 7 8 9 10 11
   |  import com.example.plugin.model.User; import org.apache.ibatis.annotations.Mapper; import java.util.List;
  @Mapper public interface UserMapper {     User selectById(Long id);     List<User> selectAll();     void insertUser(User user); }
 
  | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   | <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper         PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"         "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.plugin.mapper.UserMapper">
      <select id="selectById" resultType="User">         SELECT id, name, phone, id_card FROM user WHERE id = #{id}     </select>
      <select id="selectAll" resultType="User">         SELECT id, name, phone, id_card FROM user     </select>
  </mapper>
   |