用 Java 注解对结果集中包含身份证号码的数据进行脱敏处理

文章转载自:用 Java 注解对结果集中包含身份证号码的数据进行脱敏处理

背景

项目涉及导用户手机号码、身份证号等隐私信息,因此期望对响应到客户端结果集中的敏感信息进行脱敏处理,如身份证号码中间 8 位用*代替,手机号码中间 4 位用*代替

处理思路

  1. 定义枚举类,处理不同类型的数据(身份证号、手机号、邮件等)
  2. 定义注解,用在要处理的字段上,关键是该注解上使用 @JacksonAnnotationsInside 主键,因为 springboot 项目默认使用 jackson 序列化数据(题外话:如果使用 FastJsonHttpMessageConverter 序列化数据,则需要实现 com.alibaba.fastjson.serializer.ValueFilter 类)
  3. 定义实现类,extends JsonSerializer implements ContextualSerializer
  4. 脱敏处理帮助类

具体实现

具体实现代码和上面思路一样,由 4 个类完成,最后是要脱敏的实体类

脱敏类型枚举类

/**
 * @Author ScarletDrop
 * @Time 2020-12-16 19:14
 * @Description 需要脱敏字段类型,对以下类型都可以自定义处理
 */
public enum DesensitionType {

  //用户id
  USER_ID,
  //中文名
  CHINESE_NAME,
  //身份证号
  ID_CARD,
  //座机号
  FIXED_PHONE,
  //手机号
  MOBILE_PHONE,
  //地址
  ADDRESS,
  //电子邮件
  EMAIL,
  //密码
  PASSWORD;
}

脱敏注解类

import com.kanade.demo.common.enums.DesensitionType;
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;

/**
 * @Author ScarletDrop
 * @Time 2020-12-16 19:16
 * @Description 脱敏注解
 */
@JacksonAnnotationsInside
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JsonSerialize(using = DesensitizationSerialize.class)
public @interface Desensitization {

  /**
    * 脱敏类型规则
    * @return
    */
  DesensitionType value();

}

脱敏具体实现类

import com.kanade.demo.common.enums.DesensitionType;
import com.kanade.demo.common.utils.DesensitionType;
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 java.io.IOException;

/**
 * @Author ScarletDrop
 * @Time 2020-12-16 19:18
 * @Description 自定义脱敏序列化类
 */
public class DesensitizationSerialize extends JsonSerializer implements ContextualSerializer {

  private DesensitionType type;

  public DesensitizationSerialize(){}

  public DesensitizationSerialize(final DesensitionType type){
    this.type = type;
  }

  @Override
  public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty beanProperty) throws JsonMappingException {
    if(beanProperty != null){
      //获取字段是否有脱敏注解,有则创建一个序列化对象,并调用serialize方法
      Desensitization desensitization = beanProperty.getAnnotation(Desensitization.class);
      if(desensitization == null){
        desensitization = beanProperty.getContextAnnotation(Desensitization.class);
      }
      // 如果定义了脱敏注解,就将需要脱敏的类型传入DesensitizationSerialize构造函数
      if(desensitization != null){
        return new DesensitizationSerialize(desensitization.value());
      }
      return provider.findValueSerializer(beanProperty.getType() , beanProperty);
    }
    return provider.findNullValueSerializer(beanProperty);
  }

  @Override
  public void serialize(Object value, JsonGenerator jsonGenerator, SerializerProvider serializers) throws IOException {
    switch (this.type){
      case USER_ID:
        jsonGenerator.writeNumber(DesensitizedUtils.userId());
        break;
      case CHINESE_NAME:
        jsonGenerator.writeString(DesensitizedUtils.chineseName(String.valueOf(value)));
        break;
      case ID_CARD:
        jsonGenerator.writeString(DesensitizedUtils.idCardNum(String.valueOf(value),3,3));
        break;
      case FIXED_PHONE:
        jsonGenerator.writeString(DesensitizedUtils.fixedPhone(String.valueOf(value)));
        break;
      case MOBILE_PHONE:
        jsonGenerator.writeString(DesensitizedUtils.mobilePhone(String.valueOf(value)));
        break;
      case ADDRESS:
        jsonGenerator.writeString(DesensitizedUtils.address(String.valueOf(value), 8));
        break;
      case EMAIL:
        jsonGenerator.writeString(DesensitizedUtils.email(String.valueOf(value)));
        break;
      case PASSWORD:
        jsonGenerator.writeString(DesensitizedUtils.password(String.valueOf(value)));
        break;
      default:
    }
  }
}

脱敏处理帮助类

import org.apache.commons.lang.StringUtils;

/**
 * @Author ScarletDrop
 * @Time 2020-12-16 19:21
 * @Description 脱敏规则具体实现
 */
public class DesensitizedUtils {

  /**
    * 【用户id】不对外提供userId
    * @return
    */
  public static Long userId(){
    return Long.valueOf(0);
  }

  /**
    * 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**
    *
    * @param fullName
    * @return
    */
  public static String chineseName(String fullName) {
    if (StringUtils.isBlank(fullName)) {
      return "";
    }
    String name = StringUtils.left(fullName, 1);
    return StringUtils.rightPad(name, StringUtils.length(fullName), "*");
  }

  /**
    * 【身份证号】前三位 和后三位
    *
    * @param front
    * @param end
    * @return
    */
  public static String idCardNum(String idCardNum, int front, int end) {
    //身份证不能为空
    if (StringUtils.isEmpty(idCardNum)) {
      return "";
    }
    //需要截取的长度不能大于身份证号长度
    if ((front + end) > idCardNum.length()) {
      return "";
    }
    //需要截取的不能小于0
    if (front < 0 || end < 0) {
      return "";
    }
    //计算*的数量
    int asteriskCount = idCardNum.length() - (front + end);
    StringBuffer asteriskStr = new StringBuffer();
    for (int i = 0; i < asteriskCount; i++) {
      asteriskStr.append("*");
    }
    String regex = "(\\w{" + String.valueOf(front) + "})(\\w+)(\\w{" + String.valueOf(end) + "})";
    return idCardNum.replaceAll(regex, "$1" + asteriskStr + "$3");
  }

  /**
    * 【固定电话 前四位,后两位
    *
    * @param num
    * @return
    */
  public static String fixedPhone(String num) {
    if (StringUtils.isBlank(num)) {
      return "";
    }
    return StringUtils.left(num, 4).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(num, 2), StringUtils.length(num), "*"), "****"));
  }

  /**
    * 【手机号码】前三位,后两位,其他隐藏,比如135******10
    *
    * @param num
    * @return
    */
  public static String mobilePhone(String num) {
    if (StringUtils.isBlank(num)) {
      return "";
    }
    return StringUtils.left(num, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(num, 2), StringUtils.length(num), "*"), "***"));
  }

  /**
    * 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区****
    *
    * @param address
    * @param sensitiveSize 敏感信息长度
    * @return
    */
  public static String address(String address, int sensitiveSize) {
    if (StringUtils.isBlank(address)) {
      return "";
    }
    int length = StringUtils.length(address);
    return StringUtils.rightPad(StringUtils.left(address, length - sensitiveSize), length, "*");
  }

  /**
    * 【电子邮箱 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com>
    *
    * @param email
    * @return
    */
  public static String email(String email) {
    if (StringUtils.isBlank(email)) {
      return "";
    }
    int index = StringUtils.indexOf(email, "@");
    if (index <= 1) {
      return email;
    } else {
      return StringUtils.rightPad(StringUtils.left(email, 1), index, "*").concat(StringUtils.mid(email, index, StringUtils.length(email)));
    }
  }

  /**
    * 【密码】密码的全部字符都用*代替,比如:******
    *
    * @param password
    * @return
    */
  public static String password(String password) {
    if (StringUtils.isBlank(password)) {
      return "";
    }
    String pwd = StringUtils.left(password, 0);
    return StringUtils.rightPad(pwd, StringUtils.length(password), "*");
  }
}

要脱敏处理实体类,使用自定义脱敏注解

@Data
@Accessors(chain = true)
public class XXXXXPo {

  @Id
  @ApiModelProperty(value = "主键")
  private Long affairsInfoId;

  @ApiModelProperty(value = "身份证号")
  @Desensitization(value = DesensitionType.ID_CARD)
  private String credentialNo;

  @ApiModelProperty("用户电话")
  @Desensitization(value = DesensitionType.MOBILE_PHONE)
  private String phone;

  @ApiModelProperty("邮箱")
  @Desensitization(value = DesensitionType.EMAIL)
  private String email;
 }

简单用法就是这些,可以自形扩展,比如自定义 id 取哪几位等等

Q.E.D.