数据脱敏是指在数据查询后,对敏感字段(如手机号、身份证号、邮箱等)进行部分隐藏或替换,以保护用户隐私。以下是通过 MyBatis 插件实现数据脱敏的完整方案:
一、实现思路
- 自定义脱敏注解:在实体类字段上标记需要脱敏的字段,并指定脱敏策略。
- 编写 MyBatis 插件:拦截结果集处理逻辑,对标记的字段进行脱敏。
- 反射处理字段值:通过反射获取字段值,应用脱敏规则后重新赋值。
二、具体实现步骤
1. 定义脱敏注解
创建 @SensitiveField 注解,支持多种脱敏策略(如手机号、身份证、邮箱等):
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface SensitiveField {
// 脱敏策略类型
SensitiveType strategy() default SensitiveType.DEFAULT;
}
// 脱敏策略枚举
public enum SensitiveType {
DEFAULT, // 默认(如全替换为*)
PHONE, // 手机号(如 138****1234)
ID_CARD, // 身份证(如 420***********1234)
EMAIL // 邮箱(如 a***@example.com)
}
2. 定义脱敏工具类
实现不同策略的脱敏逻辑:
public class SensitiveUtils {
// 默认脱敏:全部替换为 *
public static String defaultMask(String value) {
if (value == null) return null;
return value.replaceAll(".", "*");
}
// 手机号脱敏:保留前3位和后4位
public static String phoneMask(String phone) {
if (phone == null || phone.length() < 7) return phone;
return phone.replaceAll("(\d{3})\d{4}(\d{4})", "$1****$2");
}
// 身份证脱敏:保留前3位和后4位
public static String idCardMask(String idCard) {
if (idCard == null || idCard.length() < 8) return idCard;
return idCard.replaceAll("(?<=\w{3})\w(?=\w{4})", "*");
}
// 邮箱脱敏:用户名部分首尾可见,中间替换为*
public static String emailMask(String email) {
if (email == null || !email.contains("@")) return email;
String[] parts = email.split("@");
if (parts[0].length() < 2) return email;
String username = parts[0].charAt(0) + "***" + parts[0].charAt(parts[0].length() - 1);
return username + "@" + parts[1];
}
}
3. 编写 MyBatis 插件
拦截 ResultSetHandler 的结果处理方法,对返回的实体对象进行脱敏:
@Intercepts({
@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class}
)
})
public class SensitiveDataPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 执行原方法获取查询结果
List<Object> results = (List<Object>) invocation.proceed();
if (results == null || results.isEmpty()) return results;
// 遍历结果中的每个对象进行脱敏
for (Object result : results) {
processSensitiveFields(result);
}
return results;
}
// 处理对象的敏感字段
private void processSensitiveFields(Object obj) {
if (obj == null) return;
Class<?> clazz = obj.getClass();
// 遍历所有字段
for (Field field : clazz.getDeclaredFields()) {
SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
if (sensitiveField == null) continue;
// 获取字段值并脱敏
field.setAccessible(true);
try {
Object value = field.get(obj);
if (value instanceof String) {
String maskedValue = mask((String) value, sensitiveField.strategy());
field.set(obj, maskedValue);
}
} catch (IllegalAccessException e) {
throw new RuntimeException("脱敏处理失败", e);
}
}
}
// 根据策略选择脱敏方法
private String mask(String value, SensitiveType strategy) {
switch (strategy) {
case PHONE:
return SensitiveUtils.phoneMask(value);
case ID_CARD:
return SensitiveUtils.idCardMask(value);
case EMAIL:
return SensitiveUtils.emailMask(value);
default:
return SensitiveUtils.defaultMask(value);
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可读取配置参数(如是否启用脱敏)
}
}
4. 注册插件
在 mybatis-config.xml 中添加插件配置:
<plugins>
<plugin interceptor="com.example.SensitiveDataPlugin"/>
</plugins>
5. 在实体类中标记脱敏字段
public class User {
private String name;
@SensitiveField(strategy = SensitiveType.PHONE)
private String phone;
@SensitiveField(strategy = SensitiveType.ID_CARD)
private String idCard;
@SensitiveField(strategy = SensitiveType.EMAIL)
private String email;
// Getters and Setters
}
三、测试效果
执行查询后,返回的 User 对象会自动脱敏:
User user = userMapper.selectUserById(1);
System.out.println(user.getPhone()); // 输出:138****1234
System.out.println(user.getIdCard()); // 输出:420***********1234
System.out.println(user.getEmail()); // 输出:a***@example.com
四、优化与注意事项
-
性能优化:
- 缓存反射操作的
Field对象,避免重复解析。 - 使用
@SensitiveField的类提前预扫描并生成脱敏元数据。
- 缓存反射操作的
-
支持嵌套对象:
private void processSensitiveFields(Object obj) { if (obj == null) return; // 处理当前对象 Class<?> clazz = obj.getClass(); for (Field field : clazz.getDeclaredFields()) { // ... 脱敏逻辑 ... // 处理嵌套对象(如 User 中的 Address 属性) Object nestedObj = field.get(obj); if (nestedObj != null && !isJavaClass(nestedObj.getClass())) { processSensitiveFields(nestedObj); } } } private boolean isJavaClass(Class<?> clazz) { return clazz.getClassLoader() == null; } -
日志与异常处理:
- 添加日志记录脱敏操作。
- 捕获反射异常并转换为明确的业务异常。
五、总结
通过 MyBatis 插件实现数据脱敏的优势:
- 无侵入性:无需修改现有 DAO 或 Mapper 代码。
- 灵活扩展:通过注解支持多种脱敏策略,可自定义规则。
- 集中管理:所有脱敏逻辑集中在插件中,便于维护。
适用场景:
- 用户隐私数据(如手机号、身份证)展示时的保护。
- 日志输出中敏感字段的自动过滤。
- 数据导出或接口返回前的脱敏处理。