0%

MyBatis 数据脱敏插件的实现

数据脱敏(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,
/**
* 手机号脱敏:138****1234
*/
PHONE,
/**
* 身份证号脱敏:340***********123X
*/
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 {

/**
* 手机号脱敏:138****1234
*/
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");
}

/**
* 身份证号脱敏:340***********123X
*/
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;

// Getters and Setters
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.*;

// 拦截 ResultSetHandler 的 'handleResultSets' 方法
@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) {
// 检查字段是否有 @SensitiveData 注解
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 -> {
// 将拦截器添加到 MyBatis 配置中
configuration.addInterceptor(dataMaskingInterceptor());
};
}
}

6. 编写 Mapper 接口和 XML

1
2
3
4
5
6
7
8
9
10
11
// UserMapper.java
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>