Java数据脱敏的注解式实现方案详解

Java数据脱敏通过注解式实现,保护敏感信息不被随意暴露。1.定义@sensitiveinfo注解标记需脱敏字段,并配置脱敏类型及策略;2.编写工具类sensitiveinfoutils,实现常见脱敏逻辑如中文名、身份证号、手机号等的处理;3.使用aop切面拦截方法返回值,遍历对象字段并根据注解配置执行脱敏;4.支持嵌套对象脱敏,递归处理集合、数组及复杂对象中的敏感字段;5.支持自定义脱敏策略,通过扩展sensitivetype枚举和实现sensitivehandler接口定义个性化规则;6.优化性能,采用缓存、异步处理、并行流等方式提升脱敏效率。

Java数据脱敏的注解式实现方案详解

Java数据脱敏,本质上就是保护敏感信息,避免在不必要的场景下暴露真实数据。注解式实现,则提供了一种优雅、便捷的方式来完成这个任务。

Java数据脱敏的注解式实现方案详解

解决方案

注解式脱敏的核心思想是:通过自定义注解标记需要脱敏的字段,然后在数据序列化或展示前,利用AOP或反射等机制,根据注解配置的脱敏策略对字段值进行处理。

  1. 定义脱敏注解:

    立即学习Java免费学习笔记(深入)”;

    Java数据脱敏的注解式实现方案详解

    import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;  @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface SensitiveInfo {     SensitiveType type() default SensitiveType.NONE; // 默认不脱敏     String replaceChar() default "*"; // 默认替换字符     int prefixNoMaskLen() default 0; // 前置不需要打码的长度     int suffixNoMaskLen() default 0; // 后置不需要打码的长度 }

    这里SensitiveType是一个枚举,定义了常见的脱敏类型,例如:

    public enum SensitiveType {     NONE, // 不脱敏     CHINESE_NAME, // 中文名     ID_CARD, // 身份证号     FIXED_PHONE, // 固定电话     MOBILE_PHONE, // 手机号     ADDRESS, // 地址     EMaiL, // 邮箱     BANK_CARD, // 银行卡     PASSWORD, // 密码     CUSTOM // 自定义 }

    replaceChar指定替换字符,prefixNoMaskLen和suffixNoMaskLen分别指定前后保留的长度。

    Java数据脱敏的注解式实现方案详解

  2. 编写脱敏策略工具类:

    public class SensitiveInfoUtils {      private static final String ASTERISK = "*";      public static String chineseName(String fullName) {         if (StringUtils.isBlank(fullName)) {             return fullName;         }         String name = StringUtils.left(fullName, 1);         return StringUtils.rightPad(name, StringUtils.length(fullName), ASTERISK);     }      public static String idCard(String idCard) {         if (StringUtils.isBlank(idCard)) {             return idCard;         }         return StringUtils.left(idCard, 6).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(idCard, 4), StringUtils.length(idCard) - 6, ASTERISK), null));     }      public static String mobilePhone(String mobile) {         if (StringUtils.isBlank(mobile)) {             return mobile;         }         return StringUtils.left(mobile, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(mobile, 4), StringUtils.length(mobile) - 3, ASTERISK), null));     }      // 其他脱敏方法...      public static String custom(String str, int prefixNoMaskLen, int suffixNoMaskLen, String maskStr) {         if (StringUtils.isBlank(str)) {             return str;         }         if (prefixNoMaskLen < 0 || suffixNoMaskLen < 0) {             return str;         }         int maskLen = StringUtils.length(str) - prefixNoMaskLen - suffixNoMaskLen;         if (maskLen <= 0) {             return str;         }         String mask = StringUtils.repeat(maskStr, maskLen);         return StringUtils.left(str, prefixNoMaskLen) + mask + StringUtils.right(str, suffixNoMaskLen);     } }

    这个工具类包含了各种脱敏方法,可以根据实际需求进行扩展。

  3. 使用AOP实现自动脱敏:

    import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component;  import java.lang.reflect.Field;  @Aspect @Component public class SensitiveInfoAspect {      @Around("@annotation(com.example.SensitiveData)") // 假设有个@SensitiveData注解标记需要脱敏的方法     public Object maskSensitiveData(ProceedingJoinPoint joinPoint) throws Throwable {         Object result = joinPoint.proceed();         if (result != null) {             processObject(result);         }         return result;     }      private void processObject(Object obj) throws IllegalAccessException {         Class<?> clazz = obj.getClass();         Field[] fields = clazz.getDeclaredFields();         for (Field field : fields) {             if (field.isAnnotationPresent(SensitiveInfo.class)) {                 SensitiveInfo sensitiveInfo = field.getAnnotation(SensitiveInfo.class);                 field.setAccessible(true);                 Object value = field.get(obj);                 if (value instanceof String) {                     String maskedValue = maskString(sensitiveInfo, (String) value);                     field.set(obj, maskedValue);                 } // 可以添加对其他类型数据的支持             }         }     }      private String maskString(SensitiveInfo sensitiveInfo, String value) {         if (StringUtils.isBlank(value)) {             return value;         }         switch (sensitiveInfo.type()) {             case CHINESE_NAME:                 return SensitiveInfoUtils.chineseName(value);             case ID_CARD:                 return SensitiveInfoUtils.idCard(value);             case MOBILE_PHONE:                 return SensitiveInfoUtils.mobilePhone(value);             case CUSTOM:                 return SensitiveInfoUtils.custom(value, sensitiveInfo.prefixNoMaskLen(), sensitiveInfo.suffixNoMaskLen(), sensitiveInfo.replaceChar());             // 其他类型...             default:                 return value; // NONE类型不脱敏         }     } }

    这个AOP切面拦截带有@SensitiveData注解的方法,获取返回值,然后遍历对象的字段,如果字段带有@SensitiveInfo注解,则根据注解配置的类型进行脱敏。需要注意的是,为了访问私有字段,需要设置field.setAccessible(true)。

  4. 在实体类中使用注解:

    public class User {     private String name;      @SensitiveInfo(type = SensitiveType.ID_CARD)     private String idCard;      @SensitiveInfo(type = SensitiveType.MOBILE_PHONE)     private String phone;      @SensitiveInfo(type = SensitiveType.ADDRESS)     private String address;      // getter 和 setter 方法 }

    现在,当带有@SensitiveData注解的方法返回User对象时,idCard和phone字段会自动脱敏。

如何处理嵌套对象的数据脱敏?

嵌套对象脱敏,简单来说,就是当你的实体类中包含其他实体类作为字段时,如何递归地对这些嵌套对象中的敏感数据进行脱敏。

修改AOP切面中的processObject方法,使其能够递归处理嵌套对象:

private void processObject(Object obj) throws IllegalAccessException {     if (obj == null) {         return;     }      Class<?> clazz = obj.getClass();      // 如果是集合类型,则遍历集合中的元素进行处理     if (obj instanceof Collection) {         Collection<?> collection = (Collection<?>) obj;         for (Object item : collection) {             processObject(item); // 递归处理集合中的每个元素         }         return;     }      // 如果是数组类型,则遍历数组中的元素进行处理     if (obj.getClass().isArray()) {         Object[] array = (Object[]) obj;         for (Object item : array) {             processObject(item); // 递归处理数组中的每个元素         }         return;     }       Field[] fields = clazz.getDeclaredFields();     for (Field field : fields) {         field.setAccessible(true);         Object fieldValue = field.get(obj);          if (field.isAnnotationPresent(SensitiveInfo.class)) {             SensitiveInfo sensitiveInfo = field.getAnnotation(SensitiveInfo.class);             if (fieldValue instanceof String) {                 String maskedValue = maskString(sensitiveInfo, (String) fieldValue);                 field.set(obj, maskedValue);             }         } else if (fieldValue != null && !field.getType().isPrimitive() &&                    !(fieldValue instanceof String) && !(fieldValue instanceof number) &&                    !(fieldValue instanceof Boolean) && !(fieldValue instanceof Enum)) {             // 递归处理嵌套对象             processObject(fieldValue);         }     } }

这里,我们添加了对集合和数组类型的判断,并递归调用processObject方法处理集合和数组中的每个元素。对于非基本类型、非String/Number/Boolean/Enum类型的字段,我们也递归调用processObject方法,实现嵌套对象的脱敏。

如何支持自定义脱敏策略?

自定义脱敏策略允许开发者根据特定业务需求,灵活地定义脱敏规则。

  1. 扩展SensitiveType枚举:

    添加一个CUSTOM类型,并允许用户指定自定义的脱敏方法。

    public enum SensitiveType {     NONE,     CHINESE_NAME,     ID_CARD,     // ...     CUSTOM }
  2. 修改SensitiveInfo注解:

    添加一个customHandler属性,用于指定自定义脱敏策略的类。

    import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;  @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface SensitiveInfo {     SensitiveType type() default SensitiveType.NONE;     Class<? extends SensitiveHandler> customHandler() default DefaultSensitiveHandler.class; // 默认使用默认处理器 }

    这里SensitiveHandler是一个接口,定义了自定义脱敏策略的方法:

    public interface SensitiveHandler {     String mask(String value); }

    DefaultSensitiveHandler是一个默认实现,不做任何脱敏处理:

    public class DefaultSensitiveHandler implements SensitiveHandler {     @Override     public String mask(String value) {         return value;     } }
  3. 修改AOP切面:

    在maskString方法中,根据SensitiveType选择对应的脱敏策略。

    private String maskString(SensitiveInfo sensitiveInfo, String value) {     if (StringUtils.isBlank(value)) {         return value;     }     if (sensitiveInfo.type() == SensitiveType.CUSTOM) {         try {             SensitiveHandler handler = sensitiveInfo.customHandler().getDeclaredConstructor().newInstance();             return handler.mask(value);         } catch (Exception e) {             // 处理异常,例如记录日志             return value; // 发生异常时,返回原始值         }     }     // 其他类型...     return value; }
  4. 使用自定义脱敏策略:

    创建一个自定义的脱敏策略类,实现SensitiveHandler接口。

    public class MyCustomSensitiveHandler implements SensitiveHandler {     @Override     public String mask(String value) {         // 实现自定义的脱敏逻辑         if (value != null && value.length() > 4) {             return "prefix_" + value.substring(value.length() - 4);         }         return "default_value";     } }

    在实体类中使用@SensitiveInfo注解,指定customHandler属性。

    public class User {     @SensitiveInfo(type = SensitiveType.CUSTOM, customHandler = MyCustomSensitiveHandler.class)     private String customField; }

如何处理性能问题?

脱敏操作,特别是当数据量很大时,可能会对性能产生影响。

  1. 缓存脱敏结果:

    对于一些静态数据,例如枚举值,可以缓存脱敏结果,避免重复计算。可以使用guava Cache或Caffeine等缓存库。

  2. 异步脱敏:

    对于一些非实时性要求的数据,可以采用异步脱敏的方式,将脱敏操作放入消息队列中,由专门的消费者进行处理。

  3. 选择合适的脱敏算法

    不同的脱敏算法的性能差异很大,例如,使用正则表达式进行脱敏的性能通常比使用字符串截取的性能要差。

  4. 避免过度脱敏:

    只对真正需要脱敏的数据进行脱敏,避免对所有数据都进行脱敏。

  5. 优化AOP切面:

    AOP切面的性能也很重要,可以优化切面表达式,减少切面的执行次数。

  6. 批量脱敏:

    如果需要对大量数据进行脱敏,可以采用批量脱敏的方式,例如,一次性处理1000条数据。

  7. 使用并行流:

    对于集合类型的数据,可以使用Java 8的并行流进行脱敏,提高脱敏效率。

    List<User> users = ...; users.parallelStream().forEach(user -> {     // 对user对象进行脱敏 });

© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享