全部学科
Python全栈
python
NodeJS全栈
nodejs
小程序首页
📅 2026-05-20 8 分钟 ✍️ juanwangdev

数据脱敏插件

数据脱敏插件通过拦截 ResultSetHandler.handleResultSets() 在结果集返回前对敏感字段进行加解密处理,对业务代码完全透明。

脱敏架构设计

Java
┌──────────────────────────────────────────────────────┐
│                 DataMaskPlugin                        │
├──────────────────────┬───────────────────────────────┤
│ 写入脱敏(插入/更新) │  读取脱敏(查询结果)           │
│ ParameterHandler 拦截 │  ResultSetHandler 拦截         │
│ - 敏感字段加密后写入   │  - 敏感字段解密后返回           │
└──────────────────────┴───────────────────────────────┘
         │                        │
    ┌────▼────┐            ┌─────▼─────┐
    │ 加密策略 │            │ 脱敏规则   │
    │ AES加密  │            │ 手机号 *** │
    │ 身份证 **│            │ 姓名 *某    │
    │ 地址脱敏 │            │ 邮箱 *      │
    └─────────┘            └───────────┘

读取脱敏:拦截 ResultSetHandler

核心实现

Java
@Intercepts({
    @Signature(type = ResultSetHandler.class, method = "handleResultSets",
        args = {Statement.class})
})
public class DataMaskPlugin implements Interceptor {

    private final MaskStrategyFactory strategyFactory = new MaskStrategyFactory();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 执行原始查询
        List<Object> results = (List<Object>) invocation.proceed();

        // 2. 对结果集进行脱敏处理
        if (results != null && !results.isEmpty()) {
            MappedStatement ms = getMappedStatement(invocation);
            for (Object result : results) {
                maskObject(result, ms);
            }
        }

        return results;
    }

    private void maskObject(Object obj, MappedStatement ms) {
        // 检查类级别脱敏注解
        MaskClass maskClass = obj.getClass().getAnnotation(MaskClass.class);
        if (maskClass == null) return;

        // 遍历字段,处理带脱敏注解的字段
        for (Field field : getAllFields(obj.getClass())) {
            MaskField maskField = field.getAnnotation(MaskField.class);
            if (maskField == null) continue;

            field.setAccessible(true);
            try {
                Object value = field.get(obj);
                if (value != null) {
                    MaskStrategy strategy = strategyFactory.getStrategy(maskField.type());
                    field.set(obj, strategy.mask(value.toString(), maskField.rule()));
                }
            } catch (Exception e) {
                // 脱敏失败,保留原值,不影响查询
            }
        }
    }
}

脱敏注解定义

Java
// 类级别注解,标记需要脱敏的实体
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MaskClass {
    String value() default "";
}

// 字段级别注解,定义脱敏类型和规则
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MaskField {
    MaskType type();     // 脱敏类型
    String rule() default "";  // 自定义规则
}

public enum MaskType {
    PHONE,      // 手机号
    ID_CARD,    // 身份证
    EMAIL,      // 邮箱
    NAME,       // 姓名
    ADDRESS,    // 地址
    BANK_CARD,  // 银行卡
    CUSTOM      // 自定义
}

实体类使用示例

Java
@MaskClass
public class User {
    private Long id;

    @MaskField(type = MaskType.NAME)
    private String name;

    @MaskField(type = MaskType.PHONE, rule = "3-4")  // 前3后4保留
    private String phone;

    @MaskField(type = MaskType.ID_CARD, rule = "6-4")
    private String idCard;

    @MaskField(type = MaskType.EMAIL)
    private String email;
}

脱敏策略实现

策略接口

Java
public interface MaskStrategy {
    /**
     * 对值进行脱敏
     * @param value 原始值
     * @param rule  规则参数(如 "3-4")
     * @return 脱敏后的值
     */
    String mask(String value, String rule);
}

手机号脱敏

Java
public class PhoneMaskStrategy implements MaskStrategy {

    @Override
    public String mask(String value, String rule) {
        // 规则 "3-4":保留前3后4,中间用 **** 替换
        if (rule != null && rule.matches("\\d+-\\d+")) {
            String[] parts = rule.split("-");
            int start = Integer.parseInt(parts[0]);
            int end = Integer.parseInt(parts[1]);
            if (value.length() >= start + end) {
                return value.substring(0, start) + "****" + value.substring(value.length() - end);
            }
        }
        // 默认:保留前3后4
        if (value.length() >= 7) {
            return value.substring(0, 3) + "****" + value.substring(value.length() - 4);
        }
        return "***";
    }
}

身份证脱敏

Java
public class IdCardMaskStrategy implements MaskStrategy {

    @Override
    public String mask(String value, String rule) {
        // 规则 "6-4":保留前6后4
        if (rule != null && rule.matches("\\d+-\\d+")) {
            String[] parts = rule.split("-");
            int start = Integer.parseInt(parts[0]);
            int end = Integer.parseInt(parts[1]);
            if (value.length() >= start + end) {
                int maskLen = value.length() - start - end;
                return value.substring(0, start)
                    + "*".repeat(maskLen)
                    + value.substring(value.length() - end);
            }
        }
        // 默认:保留前6后4
        if (value.length() == 18) {
            return value.substring(0, 6) + "********" + value.substring(14);
        }
        return "******************";
    }
}

姓名脱敏

Java
public class NameMaskStrategy implements MaskStrategy {

    @Override
    public String mask(String value, String rule) {
        if (value.isEmpty()) return "";
        if (value.length() == 1) return "*";
        if (value.length() == 2) return value.charAt(0) + "*";
        // 3字及以上:保留首尾,中间用 * 替换
        return value.charAt(0) + "*".repeat(value.length() - 2) + value.charAt(value.length() - 1);
    }
}

邮箱脱敏

Java
public class EmailMaskStrategy implements MaskStrategy {

    @Override
    public String mask(String value, String rule) {
        int atIndex = value.indexOf('@');
        if (atIndex <= 0) return "***@***.com";

        String localPart = value.substring(0, atIndex);
        String domain = value.substring(atIndex);

        if (localPart.length() <= 1) {
            return "*" + domain;
        }
        return localPart.charAt(0) + "***" + domain;
    }
}

策略工厂

Java
public class MaskStrategyFactory {

    private static final Map<MaskType, MaskStrategy> STRATEGY_MAP = new HashMap<>();

    static {
        STRATEGY_MAP.put(MaskType.PHONE, new PhoneMaskStrategy());
        STRATEGY_MAP.put(MaskType.ID_CARD, new IdCardMaskStrategy());
        STRATEGY_MAP.put(MaskType.EMAIL, new EmailMaskStrategy());
        STRATEGY_MAP.put(MaskType.NAME, new NameMaskStrategy());
        STRATEGY_MAP.put(MaskType.ADDRESS, new AddressMaskStrategy());
        STRATEGY_MAP.put(MaskType.BANK_CARD, new BankCardMaskStrategy());
        STRATEGY_MAP.put(MaskType.CUSTOM, new CustomMaskStrategy());
    }

    public MaskStrategy getStrategy(MaskType type) {
        return STRATEGY_MAP.getOrDefault(type, new CustomMaskStrategy());
    }
}

写入脱敏:拦截 ParameterHandler

插入/更新时对敏感字段加密存储:

Java
@Intercepts({
    @Signature(type = ParameterHandler.class, method = "setParameters",
        args = {PreparedStatement.class})
})
public class DataEncryptPlugin implements Interceptor {

    private final EncryptStrategyFactory encryptFactory = new EncryptStrategyFactory();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        ParameterHandler handler = (ParameterHandler) invocation.getTarget();

        // 在 setParameters 之前加密敏感字段
        Object parameterObject = getParameterObject(handler);
        if (parameterObject != null) {
            encryptObject(parameterObject);
        }

        return invocation.proceed();
    }

    private void encryptObject(Object obj) {
        EncryptClass encryptClass = obj.getClass().getAnnotation(EncryptClass.class);
        if (encryptClass == null) return;

        for (Field field : getAllFields(obj.getClass())) {
            EncryptField encryptField = field.getAnnotation(EncryptField.class);
            if (encryptField == null) continue;

            field.setAccessible(true);
            try {
                Object value = field.get(obj);
                if (value != null) {
                    EncryptStrategy strategy = encryptFactory.getStrategy(encryptField.type());
                    field.set(obj, strategy.encrypt(value.toString()));
                }
            } catch (Exception e) {
                // 加密失败,保留原值
            }
        }
    }
}

加密注解

Java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptClass {}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {
    EncryptType type();
}

public enum EncryptType {
    AES, RSA, SM4
}

AES 加密策略

text
public class AESEncryptStrategy implements EncryptStrategy {

    private final String secretKey;

    public AESEncryptStrategy(String secretKey) {
        this.secretKey = secretKey;
    }

    @Override
    public String encrypt(String value) {
        try {
            SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), "AES");
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);
            byte[] encrypted = cipher.doFinal(value.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("AES encrypt failed", e);
        }
    }

    @Override
    public String decrypt(String value) {
        try {
            SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), "AES");
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            byte[] decoded = Base64.getDecoder().decode(value);
            byte[] decrypted = cipher.doFinal(decoded);
            return new String(decrypted, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("AES decrypt failed", e);
        }
    }
}

脱敏策略对比表

脱敏类型处理方式可逆性能适用场景
掩码替换138****1234极高展示、日志、导出
AES 加密Base64(encrypt(value))数据库存储
哈希处理SHA256(value + salt)查询匹配(如密码)
格式保留加密FPE(138****1234)需保持格式的字段
数据泛化年龄 25 → 20-30极高统计分析

注意事项

  1. 脱敏范围:仅处理带注解的字段,未注解字段不受影响,避免误脱敏
  2. 性能影响:反射操作字段和读写值有一定开销,大数据量结果集建议批量处理
  3. 异常容错:脱敏/加密失败应保留原值,不阻断查询流程
  4. 嵌套对象:递归处理集合、嵌套对象中的脱敏注解字段
  5. ThreadLocal 开关:可通过 ThreadLocal 控制是否启用脱敏,支持不同环境切换

要点总结

  • 读取脱敏通过拦截 ResultSetHandler.handleResultSets(),在结果集返回前对敏感字段执行脱敏
  • 写入脱敏通过拦截 ParameterHandler.setParameters(),在参数绑定前对敏感字段执行加密
  • 脱敏策略通过 MaskStrategy 接口统一抽象,手机号、身份证、姓名、邮箱等各自实现
  • 注解驱动:@MaskClass 标记实体类,@MaskField(type, rule) 定义字段脱敏类型和规则
  • AES 加密用于数据库存储,掩码替换用于前端展示,哈希处理用于查询匹配,场景分离
  • 脱敏失败应容错保留原值,不阻断查询,通过反射操作字段需设置 setAccessible(true)
  • 可通过 ThreadLocal 开关控制脱敏启用,支持开发环境关闭、生产环境开启

存放路径:D:\git2\jwdev\articles\MYBATIS\专家\插件开发高级应用\数据脱敏插件.md

📝 发现内容有误?点击此处直接编辑

← 上一篇 插件代理链原理
下一篇 → 动态数据源切换
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库