数据脱敏(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>
|