数据脱敏插件
数据脱敏插件通过拦截 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 | 否 | 极高 | 统计分析 |
注意事项
- 脱敏范围:仅处理带注解的字段,未注解字段不受影响,避免误脱敏
- 性能影响:反射操作字段和读写值有一定开销,大数据量结果集建议批量处理
- 异常容错:脱敏/加密失败应保留原值,不阻断查询流程
- 嵌套对象:递归处理集合、嵌套对象中的脱敏注解字段
- ThreadLocal 开关:可通过 ThreadLocal 控制是否启用脱敏,支持不同环境切换
要点总结
- 读取脱敏通过拦截
ResultSetHandler.handleResultSets(),在结果集返回前对敏感字段执行脱敏 - 写入脱敏通过拦截
ParameterHandler.setParameters(),在参数绑定前对敏感字段执行加密 - 脱敏策略通过
MaskStrategy接口统一抽象,手机号、身份证、姓名、邮箱等各自实现 - 注解驱动:
@MaskClass标记实体类,@MaskField(type, rule)定义字段脱敏类型和规则 - AES 加密用于数据库存储,掩码替换用于前端展示,哈希处理用于查询匹配,场景分离
- 脱敏失败应容错保留原值,不阻断查询,通过反射操作字段需设置
setAccessible(true) - 可通过 ThreadLocal 开关控制脱敏启用,支持开发环境关闭、生产环境开启
存放路径:D:\git2\jwdev\articles\MYBATIS\专家\插件开发高级应用\数据脱敏插件.md
📝 发现内容有误?点击此处直接编辑