Use custom annotations in SpringBoot to elegantly realize privacy data desensitization (encrypted display)

Foreword
During the two days of rectification and other security tests, there is a risk item of "user information leakage" (that is, some private data of users in the background system is directly displayed in plain text), which actually refers to data desensitization.

Data desensitization: Encrypt some sensitive data in the system before returning it to protect privacy
desensitization effect
. Do an encryption process before returning the data. Of course, this method must be very profitable. It is recommended to use annotations to achieve this, which is efficient and elegant, saves time and effort, and supports extensions.

In fact, there are generally two solutions:
1. Desensitize the data when you get it (such as using the insert function to hide it when querying mysql)
2. Desensitize it when you get the data (such as Use fastjson, jackson)
The solution I choose here is the second one, that is, before the interface returns the data, the sensitive field values ​​are processed during serialization, and the serialization of jackson is used to achieve it (recommended)

1. Create a privacy data type enumeration: PrivacyTypeEnum

import lombok.Getter;

/**
 * 隐私数据类型枚举
 */
@Getter
public enum PrivacyTypeEnum {
    
    

  /** 自定义(此项需设置脱敏的范围)*/
  CUSTOMER,

  /** 姓名 */
  NAME,

  /** 身份证号 */
  ID_CARD,

  /** 手机号 */
  PHONE,

  /** 邮箱 */
  EMAIL,
}

2. Create a custom privacy annotation: PrivacyEncrypt

import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义数据脱敏注解
 */
@Target(ElementType.FIELD) // 作用在字段上
@Retention(RetentionPolicy.RUNTIME) // class文件中保留,运行时也保留,能通过反射读取到
@JacksonAnnotationsInside // 表示自定义自己的注解PrivacyEncrypt
@JsonSerialize(using = PrivacySerializer.class) // 该注解使用序列化的方式
public @interface PrivacyEncrypt {
    
    

    /**
     * 脱敏数据类型(没给默认值,所以使用时必须指定type)
     */
    PrivacyTypeEnum type();

    /**
     * 前置不需要打码的长度
     */
    int prefixNoMaskLen() default 1;

    /**
     * 后置不需要打码的长度
     */
    int suffixNoMaskLen() default 1;

    /**
     * 用什么打码
     */
    String symbol() default "*";
}

3. Create a custom serializer: PrivacySerializer

import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

import java.io.IOException;
import java.util.Objects;

@NoArgsConstructor
@AllArgsConstructor
public class PrivacySerializer extends JsonSerializer<String> implements ContextualSerializer {
    
    

    // 脱敏类型
    private PrivacyTypeEnum privacyTypeEnum;
    // 前几位不脱敏
    private Integer prefixNoMaskLen;
    // 最后几位不脱敏
    private Integer suffixNoMaskLen;
    // 用什么打码
    private String symbol;

    @Override
    public void serialize(String origin, final JsonGenerator jsonGenerator,
                          final SerializerProvider serializerProvider) throws IOException {
    
    
        if (StrUtil.isEmpty(origin)) {
    
    
            origin = null;
        }
        if (null != privacyTypeEnum) {
    
    
            switch (privacyTypeEnum) {
    
    
                case CUSTOMER:
                    jsonGenerator.writeString(PrivacyUtil.desValue(origin, prefixNoMaskLen, suffixNoMaskLen, symbol));
                    break;
                case NAME:
                    jsonGenerator.writeString(PrivacyUtil.hideChineseName(origin));
                    break;
                case ID_CARD:
                    jsonGenerator.writeString(PrivacyUtil.hideIDCard(origin));
                    break;
                case PHONE:
                    jsonGenerator.writeString(PrivacyUtil.hidePhone(origin));
                    break;
                case EMAIL:
                    jsonGenerator.writeString(PrivacyUtil.hideEmail(origin));
                    break;
                default:
                    throw new IllegalArgumentException("unknown privacy type enum " + privacyTypeEnum);
            }
        }
    }

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

}

Here is the specific implementation process, because the data to be desensitized is of String type, so when inheriting JsonSerializer, fill in the type of String and
rewrite the serialize method is the core of desensitization, and set the serialized value according to the type The rewritten
createContextual method is to read our custom PrivacyEncrypt annotations to create a contextual environment

[ Important ] Update on 2023-07-04: The above code has been optimized and modified to solve the abnormal problem reported by Jackson serialization when the field value in the database table is an empty string or non-null

4. Privacy data hiding tool class: PrivacyUtil

public class PrivacyUtil {
    
    

    /**
     * 中文名脱敏
     */
    public static String hideChineseName(String chineseName) {
    
    
        if (StrUtil.isEmpty(chineseName)) {
    
    
            return null;
        }
        if (chineseName.length() <= 2) {
    
    
            return desValue(chineseName, 1, 0, "*");
        }
        return desValue(chineseName, 1, 1, "*");
    }

    /**
     * 手机号脱敏
     */
    public static String hidePhone(String phone) {
    
    
        if (StrUtil.isEmpty(phone)) {
    
    
            return null;
        }
        return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); // 隐藏中间4位
//        return desValue(phone, 3, 4, "*"); // 隐藏中间4位
//        return desValue(phone, 7, 0, "*"); // 隐藏末尾4位
    }

    /**
     * 邮箱脱敏
     */
    public static String hideEmail(String email) {
    
    
        if (StrUtil.isEmpty(email)) {
    
    
            return null;
        }
        return email.replaceAll("(\\w?)(\\w+)(\\w)(@\\w+\\.[a-z]+(\\.[a-z]+)?)", "$1****$3$4");
    }

    /**
     * 身份证号脱敏
     */
    public static String hideIDCard(String idCard) {
    
    
        if (StrUtil.isEmpty(idCard)) {
    
    
            return null;
        }
        return idCard.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1*****$2");
    }

    /**
     * 对字符串进行脱敏操作
     * @param origin          原始字符串
     * @param prefixNoMaskLen 左侧需要保留几位明文字段
     * @param suffixNoMaskLen 右侧需要保留几位明文字段
     * @param maskStr         用于遮罩的字符串, 如'*'
     * @return 脱敏后结果
     */
    public static String desValue(String origin, int prefixNoMaskLen, int suffixNoMaskLen, String maskStr) {
    
    
        if (StrUtil.isEmpty(origin)) {
    
    
            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();
    }

}

In fact, this tool class can be customized by yourself and expanded according to your own business. Two points are mentioned:

  1. Regardless of the desensitization of this tool class, you must first determine whether the parameter is empty. If it is empty, return null directly, so that jsonGenerator.writeString() can be executed normally
  2. In the custom annotation PrivacyEncrypt, only when the value of type is PrivacyTypeEnum.CUSTOMER (custom), the desensitization range needs to be specified, that is, the values ​​of prefixNoMaskLen and suffixNoMaskLen, and hidden formats such as mailboxes and mobile numbers can be fixed

5. Annotation use

Directly add annotations to the fields that need to be desensitized, and specify the type value (usually used on the fields in the returned VO entity class), as follows:

@Data
public class People {
    
    

    private Integer id;

    private String name;

    private Integer sex;

    private Integer age;

    @PrivacyEncrypt(type = PrivacyTypeEnum.PHONE) // 隐藏手机号
    private String phone;

    @PrivacyEncrypt(type = PrivacyTypeEnum.EMAIL) // 隐藏邮箱
    private String email;

    private String sign;
}

At this point, the desensitization work is over. You can use this annotation globally, once and for all, and support expansion. The test effect is as follows:

Effect

The above code has been tested and used in actual production, so you can use it with confidence
Reference article: https://blog.csdn.net/a792396951/article/details/117993344

Guess you like

Origin blog.csdn.net/qq_36737803/article/details/122366043