SpringBoot uses JsonSerializer and Aop to achieve controllable data desensitization

This is a community that may be useful to you

One-to-one communication/interview brochure/resume optimization/job search questions, welcome to join the “Yudao Rapid Development Platform” Knowledge Planet. The following is some information provided by Planet:

  • “Project Practice (Video)”: Learn from books, “practice” from past events

  • “Internet High Frequency Interview Questions”: Studying with your resume, spring blossoms

  • “Architecture x System Design”: Overcoming difficulties and mastering high-frequency interview scenario questions

  • “Advancing Java Learning Guide”: systematic learning, the mainstream technology stack of the Internet

  • “Must-read Java Source Code Column”: Know what it is and why it is so

d550083fd7e6e0512aa98985e47ff49f.gif

This is an open source project that may be useful to you

Domestic Star is a 100,000+ open source project. The front-end includes management backend + WeChat applet, and the back-end supports monomer and microservice architecture.

Functions cover RBAC permissions, SaaS multi-tenancy, data permissions, mall, payment, workflow, large-screen reports, WeChat public account, etc.:

  • Boot address: https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud address: https://gitee.com/zhijiantianya/yudao-cloud

  • Video tutorial: https://doc.iocoder.cn

Source: juejin.cn/post/
7225848846785544247

  • 1 cause

  • 2 Initial attempts

  • 3 Improvements to the second version

  • 4 Final plan

48599c4b1158b0e587cb62535d30dae2.jpeg

1 Cause

Recently, I am writing a function to desensitize users’ sensitive data. After looking around on the Internet, it is basically global scope. I think it should be more flexible, and desensitization is more appropriate in different scenarios and different businesses.

For an introduction to JsonSerializer, please refer to this guy’s guide

https://juejin.cn/post/6872636051237240846

For aop introduction, please refer to this boss’s

https://juejin.cn/post/6844903575441637390

Backend management system + user applet implemented based on Spring Boot + MyBatis Plus + Vue & Element, supporting RBAC dynamic permissions, multi-tenancy, data permissions, workflow, three-party login, payment, SMS, mall and other functions

  • Project address: https://github.com/YunaiV/ruoyi-vue-pro

  • Video tutorial: https://doc.iocoder.cn/video/

2 Preliminary attempt

enum class

/**
 * Sensitive information enumeration class
 *
 **/
public enum PrivacyTypeEnum {
 
   /**
    * customize
    */
   CUSTOMER,
   /**
    * Username, Zhang*san, Li*
    */
   CHINESE_NAME,
   /**
    * ID number, 110110********1234
    */
   ID_CARD,
   /**
    * Landline number, ****1234
    */
   FIXED_PHONE,
   /**
    * Mobile phone number, 176****1234
    */
   MOBILE_PHONE,
   /**
    *Address, Beijing********
    */
   ADDRESS,
   /**
    * Email, s*****[email protected]
    */
   EMAIL,
   /**
    * Bank card, 622202************1234
    */
   BANK_CARD,
   /**
    * Password, always ******, has nothing to do with length
    */
   PASSWORD,
   /**
    * Key, always ******, regardless of length
    */
   KEY
 
}

annotation

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})//Acts on the field
@JacksonAnnotationsInside // Indicates customizing your own annotation PrivacyEncrypt
@JsonSerialize(using = PrivacySerialize.class)//This annotation uses serialization
public @interface PrivacyEncrypt {
 
   /**
    * Desensitized data type, when it is not Customer, refixNoMaskLen and suffixNoMaskLen and maskStr will be ignored
    */
   PrivacyTypeEnum type() default PrivacyTypeEnum.CUSTOMER;
 
   /**
    * The length of the front does not need to be coded
    */
   int prefixNoMaskLen() default 0;
 
   /**
    * The length of the post does not need to be coded
    */
   int suffixNoMaskLen() default 0;
 
   /**
    * What to use for coding?
    */
   String maskStr() default "*";

}

Serialization class

public class PrivacySerialize extends JsonSerializer<String> implements ContextualSerializer {
   public static final Logger logger = LoggerFactory.getLogger(PrivacySerialize.class);
   private PrivacyTypeEnum type;
 
   private Integer prefixNoMaskLen;
 
   private Integer suffixNoMaskLen;
 
   private String maskStr;

   public PrivacySerialize(PrivacyTypeEnum type, Integer prefixNoMaskLen, Integer suffixNoMaskLen, String maskStr) {
      this.type = type;
      this.prefixNoMaskLen = prefixNoMaskLen;
      this.suffixNoMaskLen = suffixNoMaskLen;
      this.maskStr = maskStr;
   }
   public PrivacySerialize() {
   }

   @Override
   public void serialize(String origin,JsonGenerator jsonGenerator,SerializerProvider serializerProvider) throws IOException {
      if (StringUtils.isNotBlank(origin) & amp; & amp; null != type) {
         switch (type) {
            case CHINESE_NAME:
               jsonGenerator.writeString(DesensitizedUtils.chineseName(origin));
               break;
            case ID_CARD:
               jsonGenerator.writeString(DesensitizedUtils.idCardNum(origin));
               break;
            case FIXED_PHONE:
               jsonGenerator.writeString(DesensitizedUtils.fixedPhone(origin));
               break;
            case MOBILE_PHONE:
               jsonGenerator.writeString(DesensitizedUtils.mobilePhone(origin));
               break;
            case ADDRESS:
               jsonGenerator.writeString(DesensitizedUtils.address(origin));
               break;
            case EMAIL:
               jsonGenerator.writeString(DesensitizedUtils.email(origin));
               break;
            case BANK_CARD:
               jsonGenerator.writeString(DesensitizedUtils.bankCard(origin));
               break;
            case PASSWORD:
               jsonGenerator.writeString(DesensitizedUtils.password(origin));
               break;
            case KEY:
               jsonGenerator.writeString(DesensitizedUtils.key(origin));
               break;
            case CUSTOMER:
               jsonGenerator.writeString(DesensitizedUtils.desValue(origin, prefixNoMaskLen, suffixNoMaskLen, maskStr));
               break;
            default:
               throw new IllegalArgumentException("Unknow sensitive type enum " + type);
         }
      }else {
         jsonGenerator.writeString("");
      }

   }

   @Override
   public JsonSerializer<?> createContextual(SerializerProvider serializerProvider,BeanProperty beanProperty) throws JsonMappingException {
      if (beanProperty != null) {
            if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
               PrivacyEncrypt encrypt = beanProperty.getAnnotation(PrivacyEncrypt.class);
               if (encrypt == null) {
                  encrypt = beanProperty.getContextAnnotation(PrivacyEncrypt.class);
               }
               if (encrypt != null) {
                  return new PrivacySerialize(encrypt.type(), encrypt.prefixNoMaskLen(),
                        encrypt.suffixNoMaskLen(), encrypt.maskStr());
               }
            }
         return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
      }
      return serializerProvider.findNullValueSerializer(null);
   }
}

Desensitization tools

/**
 * Desensitization tools
 *
 **/
public class DesensitizedUtils {
 
   /**
    * Desensitize strings
    * @param origin original string
    * @param prefixNoMaskLen How many clear text fields need to be reserved on the left side
    * @param suffixNoMaskLen How many clear text fields need to be retained on the right side?
    * @param maskStr String used for masking, such as '*'
    * @return the result after desensitization
    */
   public static String desValue(String origin, int prefixNoMaskLen, int suffixNoMaskLen, String maskStr) {
      if (origin == null) {
         return null;
      }
 
      StringBuilder sb = new StringBuilder();
      for (int i = 0, n = origin.length(); i < n; i + + ) {
         if (i < prefixNoMaskLen) {
            sb.append(origin.charAt(i));
            continue;
         }
         if (i > (n - suffixNoMaskLen - 1)) {
            sb.append(origin.charAt(i));
            continue;
         }
         sb.append(maskStr);
      }
      return sb.toString();
   }
 
   /**
    * [Chinese name] only displays the last Chinese character, and other characters are hidden as asterisks, such as: ** Meng
    * @param fullName name
    * @return result
    */
   public static String chineseName(String fullName) {
      if (fullName == null) {
         return null;
      }
      return desValue(fullName, 0, 1, "*");
   }
 
   /**
    * [ID card number] displays the first six digits and four digits, and hides the others. A total of 18 or 15 digits, for example: 340304*******1234
    * @param id ID number
    * @return result
    */
   public static String idCardNum(String id) {
      return desValue(id, 6, 4, "*");
   }
 
   /**
    * [Landline number] The last four digits, the others are hidden, such as ****1234
    * @param num landline phone
    * @return result
    */
   public static String fixedPhone(String num) {
      return desValue(num, 0, 4, "*");
   }
 
   /**
    * [Mobile phone number] The first three digits, the last four digits, and the others are hidden, such as 135****6810
    * @param num mobile phone number
    * @return result
    */
   public static String mobilePhone(String num) {
      return desValue(num, 3, 4, "*");
   }
 
   /**
    * [Address] only displays the region, not the detailed address, for example: Haidian District, Beijing****
    * @param address address
    * @return result
    */
   public static String address(String address) {
      return desValue(address, 6, 0, "*");
   }
 
   /**
    * [E-mail address: only the first letter of the email prefix is displayed, other prefixes are hidden and replaced with asterisks, @ and subsequent addresses are displayed, for example: d**@126.com
    * @param email email
    * @return result
    */
   public static String email(String email) {
      return email.replaceAll("(\w?)(\w + )(\w)(@\w + \.[a-z] + (\.[a-z] + )?)", "$1****$3 $4");
 
   }
 
   /**
    * [Bank card number] The first six digits, the last four digits, and other asterisks are used to hide each asterisk, such as: 622260************1234
    * @param cardNum bank card number
    * @return result
    */
   public static String bankCard(String cardNum) {
      return desValue(cardNum, 6, 4, "*");
   }
 
   /**
    * [Password] All characters in the password are replaced with *, for example: ******
    * @param password password
    * @return result
    */
   public static String password(String password) {
      if (password == null) {
         return null;
      }
      return "******";
   }
 
   /**
    * [Key] Except for the last three digits of the key, all keys are replaced with *, for example: ***xdS The length after desensitization is 6. If the length of the plaintext is less than three digits, it will be displayed according to the actual length, and the remaining positions will be filled with *
    * @param key key
    * @return result
    */
   public static String key(String key) {
      if (key == null) {
         return null;
      }
      int viewLength = 6;
      StringBuilder tmpKey = new StringBuilder(desValue(key, 0, 3, "*"));
      if (tmpKey.length() > viewLength) {
         return tmpKey.substring(tmpKey.length() - viewLength);
      }
      else if (tmpKey.length() < viewLength) {
         int buffLength = viewLength - tmpKey.length();
         for (int i = 0; i < buffLength; i + + ) {
            tmpKey.insert(0, "*");
         }
         return tmpKey.toString();
      }
      else {
         return tmpKey.toString();
      }
   }
 
}

Annotation usage

85f1e66e911872498392a653d59e9257.png
8e72ab47c10bc2849c7b2b5dcd274acd.png

Data desensitization has indeed been achieved, but there is a problem. The current desensitization is aimed at the fact that as long as the entity class is used to return the interface, the data in it will be desensitized. In some scenarios, it is not necessary, so it is necessary. Make improvements.

Backend management system + user applet implemented based on Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element, supporting RBAC dynamic permissions, multi-tenancy, data permissions, workflow, three-party login, payment, SMS, mall and other functions

  • Project address: https://github.com/YunaiV/yudao-cloud

  • Video tutorial: https://doc.iocoder.cn/video/

3 Second Edition Improvements

My idea is to inherit a parent class in the entity class and define a field so that it can be used as a switch for desensitization, and the entity class field does not participate in the serialization desensitization control class.

public class DataMaskKey implements Serializable {
    //Do not serialize, set key to control filtering, not enabled by default
    private transient Boolean isPrivacyKey = false;

    public Boolean getPrivacyKey() {
        return isPrivacyKey;
    }

    public void setPrivacyKey(Boolean privacyKey) {
        isPrivacyKey = privacyKey;
    }
}

The updated serialization class

The idea is to obtain the attributes of the member through reflection. Because we don’t know how many attributes will be inherited, we need to recursively search for the required fields.

public class PrivacySerialize extends JsonSerializer<String> implements ContextualSerializer {
   public static final Logger logger = LoggerFactory.getLogger(PrivacySerialize.class);
   private PrivacyTypeEnum type;
 
   private Integer prefixNoMaskLen;
 
   private Integer suffixNoMaskLen;
 
   private String maskStr;

   public PrivacySerialize(PrivacyTypeEnum type, Integer prefixNoMaskLen, Integer suffixNoMaskLen, String maskStr) {
      this.type = type;
      this.prefixNoMaskLen = prefixNoMaskLen;
      this.suffixNoMaskLen = suffixNoMaskLen;
      this.maskStr = maskStr;
   }
   public PrivacySerialize() {
   }

   @Override
   public void serialize(String origin,JsonGenerator jsonGenerator,SerializerProvider serializerProvider) throws IOException {
      boolean flag = false;
      //Reflection to get the object
      Object currentValue = jsonGenerator.getOutputContext().getCurrentValue();
      //Reflection to get class
      Class<?> aClass = jsonGenerator.getOutputContext().getCurrentValue().getClass();
      List<Field> fieldList = getFieldList(aClass);
      for (Field field : fieldList) {
         //Start reflection acquisition
         String name = field.getName();
         if ("isPrivacyKey".equals(name)){
            try {
               //Reassign
               flag = (boolean) field.get(currentValue);
            } catch (IllegalAccessException e) {
               e.printStackTrace();
            }
         }
      }
      //Reflection for switch judgment
      if (flag){
         //logger.info("Perform desensitization");
         if (StringUtils.isNotBlank(origin) & amp; & amp; null != type) {
            switch (type) {
               case CHINESE_NAME:
                  jsonGenerator.writeString(DesensitizedUtils.chineseName(origin));
                  break;
               case ID_CARD:
                  jsonGenerator.writeString(DesensitizedUtils.idCardNum(origin));
                  break;
               case FIXED_PHONE:
                  jsonGenerator.writeString(DesensitizedUtils.fixedPhone(origin));
                  break;
               case MOBILE_PHONE:
                  jsonGenerator.writeString(DesensitizedUtils.mobilePhone(origin));
                  break;
               case ADDRESS:
                  jsonGenerator.writeString(DesensitizedUtils.address(origin));
                  break;
               case EMAIL:
                  jsonGenerator.writeString(DesensitizedUtils.email(origin));
                  break;
               case BANK_CARD:
                  jsonGenerator.writeString(DesensitizedUtils.bankCard(origin));
                  break;
               case PASSWORD:
                  jsonGenerator.writeString(DesensitizedUtils.password(origin));
                  break;
               case KEY:
                  jsonGenerator.writeString(DesensitizedUtils.key(origin));
                  break;
               case CUSTOMER:
                  jsonGenerator.writeString(DesensitizedUtils.desValue(origin, prefixNoMaskLen, suffixNoMaskLen, maskStr));
                  break;
               default:
                  throw new IllegalArgumentException("Unknow sensitive type enum " + type);
            }
         }else {
            jsonGenerator.writeString("");
         }
      }else {
         //logger.info("No desensitization");
         jsonGenerator.writeString(origin);
      }

   }

   @Override
   public JsonSerializer<?> createContextual(SerializerProvider serializerProvider,BeanProperty beanProperty) throws JsonMappingException {
      if (beanProperty != null) {
            if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
               PrivacyEncrypt encrypt = beanProperty.getAnnotation(PrivacyEncrypt.class);
               if (encrypt == null) {
                  encrypt = beanProperty.getContextAnnotation(PrivacyEncrypt.class);
               }
               if (encrypt != null) {
                  return new PrivacySerialize(encrypt.type(), encrypt.prefixNoMaskLen(),
                        encrypt.suffixNoMaskLen(), encrypt.maskStr());
               }
            }
         return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
      }
      return serializerProvider.findNullValueSerializer(null);
   }

   private List<Field> getFieldList(Class<?> clazz){
      if(null == clazz){
         return null;
      }
      List<Field> fieldList = new ArrayList<>();
      //Recursively search for required fields
      Class<?> aClass = ClassRecursionUtils.getClass(clazz, "isPrivacyKey");
      Field[] declaredFields = aClass.getDeclaredFields();
      for (Field field : declaredFields) {
         if (field != null){
            //Set attribute accessibility
            field.setAccessible(true);
            //Filter static
            if(Modifier.isStatic(field.getModifiers())){
               continue;
            }
            String name = field.getName();
            //Filter non-Boolean types
            Class<?> type = field.getType();
            //And only add isPrivacyKey
            if (type.isAssignableFrom(Boolean.class) & amp; & amp; "isPrivacyKey".equals(name)){
               fieldList.add(field);
            }
         }
      }
      return fieldList;
   }

}

Recursive utility class

public class ClassRecursionUtils {
    public static Class<?> getClass(Class<?> c, String fieldName) {
        if (c !=null & amp; & amp; !hasField(c, fieldName)) {
            return getClass(c.getSuperclass(), fieldName);
        }
        return c;
    }

    public static boolean hasField(Class<?> c, String fieldName){
        Field[] fields = c.getDeclaredFields();
        for (Field f : fields) {
            if (fieldName.equals(f.getName())) {
                return true;
            }
        }
        return false;
    }
}

Now you only need to manually set it when encapsulating data in the entity class.

4 Final plan

In the above situation, it is possible to manually control whether to desensitize in certain scenarios, but the original code needs to be modified. I find it unfriendly, so I use aop for control.

There are basically two return types for projects

  • entity class as return

  • Pagination return

aop annotation

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})//Acts on the method
public @interface PrivacyKeyAnnotation {

    /**
     * Whether to enable serialized desensitization. It is enabled by default.
     */
    boolean isKey() default true;

    /**
     * Is it PageInfo<?> (paging object)
     */
    boolean isPageKey() default false;
}

aop class

@Component
@Aspect
public class PrivacyKeyAspect {
    public static final Logger logger = LoggerFactory.getLogger(PrivacyKeyAspect.class);

    /**
     * @Description: Surround notification contains this annotation
     * @param: ProceedingJoinPoint joinPoint
     * @return: Object
     */
    @Around(value = "@annotation("aop annotation address xxxxx")")
    public Object repeatSub(ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }

    /**
     * @Description: post notification
     */
    @AfterReturning(value = "@annotation("aop annotation address")",returning = "result")
    public void setPrivacyKeyType(JoinPoint joinPoint, Object result) throws Throwable {
        //Annotate value acquisition
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        //Whether to enable desensitization
        boolean flag = method.getDeclaredAnnotation(PrivacyKeyAnnotation.class).isKey();
        //Whether to desensitize paging
        boolean status = method.getDeclaredAnnotation(PrivacyKeyAnnotation.class).isPageKey();
        if (!status) {
            //Perform return value reflection
            Class<?> aClass = ClassRecursionUtils.getClass(result.getClass(), "isPrivacyKey");
            if (null != aClass) {
                setFieldMethod(result, flag, aClass);
            }
        } else {
            //Reflection paging page
            //Reflection list type
            Parameter[] parameters = signature.getMethod().getParameters();
            //generic name
            String name = parameters[0].getName();
            //generic class
            Class<?> type = parameters[0].getType();
            //Package names
            String typeName = type.getName();
            PropertyDescriptor[] ps = Introspector.getBeanInfo(result.getClass(), Object.class).getPropertyDescriptors();
            for (PropertyDescriptor prop : ps) {
                if (prop.getPropertyType().isAssignableFrom(List.class)) { //List collection type
                    Object obj = result.getClass().getMethod(prop.getReadMethod().getName()).invoke(result);
                    if(obj !=null){
                        List<?> listObj = (List<?>) obj;
                        for (Object next : listObj) {
                            Class<?> classObj = Class.forName(typeName);
                            //Get member variables
                            Class<?> keyClass = ClassRecursionUtils.getClass(classObj, "isPrivacyKey");
                            setFieldMethod(next, flag, keyClass);
                        }
                    }

                }
            }
        }
    }

    /**
     * Content filling
     */
    private void setFieldMethod(Object result, boolean flag, Class<?> aClass) throws IllegalAccessException {
        Field[] declaredFields = aClass.getDeclaredFields();
        for (Field field : declaredFields) {
            //Set attribute accessibility
            field.setAccessible(true);
            //Only get isPrivacyKey
            String name = field.getName();
            //Filter non-Boolean types
            Class<?> type = field.getType();
            //And only add isPrivacyKey
            if (type.isAssignableFrom(Boolean.class) & amp; & amp; "isPrivacyKey".equals(name)){
                //Rewrite
                field.set(result,flag);
            }
        }
    }
}

Use write on service implement class method

Finally, for another implementation method, you can refer to:

https://juejin.cn/post/7242145254057410615

Welcome to join my knowledge planet and comprehensively improve your technical capabilities.

To join, Long press” or “Scan” the QR code below:

27addda8c6cc219342c4131b875f2131.png

Planet’s content includes: project practice, interviews and recruitment, source code analysis, and learning routes.

e7edcc96d0441f09c1b1594d73c1a317.png

aefb5eb695eb8b8aacdd493564a857b2.pnga188b45dd0d45195adae5614d968aa5e.png a7a19a88141116b96d29a2745631aa84.png92ee6735ccbc7661 18d4128f57787aa0.png

If the article is helpful, please read it and forward it.
Thank you for your support (*^__^*)

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Java Skill TreeHomepageOverview 139041 people are learning the system