MyBatis 插件实现数据脱敏

77 阅读3分钟

数据脱敏是指在数据查询后,对敏感字段(如手机号、身份证号、邮箱等)进行部分隐藏或替换,以保护用户隐私。以下是通过 MyBatis 插件实现数据脱敏的完整方案:

一、实现思路

  1. 自定义脱敏注解:在实体类字段上标记需要脱敏的字段,并指定脱敏策略。
  2. 编写 MyBatis 插件:拦截结果集处理逻辑,对标记的字段进行脱敏。
  3. 反射处理字段值:通过反射获取字段值,应用脱敏规则后重新赋值。

二、具体实现步骤

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

四、优化与注意事项

  1. 性能优化

    • 缓存反射操作的 Field 对象,避免重复解析。
    • 使用 @SensitiveField 的类提前预扫描并生成脱敏元数据。
  2. 支持嵌套对象

    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;
    }
    
  3. 日志与异常处理

    • 添加日志记录脱敏操作。
    • 捕获反射异常并转换为明确的业务异常。

五、总结

通过 MyBatis 插件实现数据脱敏的优势:

  • 无侵入性:无需修改现有 DAO 或 Mapper 代码。
  • 灵活扩展:通过注解支持多种脱敏策略,可自定义规则。
  • 集中管理:所有脱敏逻辑集中在插件中,便于维护。

适用场景

  • 用户隐私数据(如手机号、身份证)展示时的保护。
  • 日志输出中敏感字段的自动过滤。
  • 数据导出或接口返回前的脱敏处理。