Backend Development 11 min read

Implementing Data Masking in Java Backend Using Custom Annotations and Jackson

This article explains how to protect user privacy by applying data desensitization in a Java backend, using a custom annotation and Jackson serializer to mask sensitive fields such as phone numbers, emails, IDs, and names during JSON serialization.

Top Architect
Top Architect
Top Architect
Implementing Data Masking in Java Backend Using Custom Annotations and Jackson

During a recent security compliance fix, the author discovered a risk of user information leakage and introduced data desensitization as a solution.

Two general approaches are described: masking data directly in the database query or masking during serialization. The article chooses the latter, using Jackson to serialize sensitive fields.

1. Create the privacy data type enum: PrivacyTypeEnum

import lombok.Getter;
/**
 * 隐私数据类型枚举
 */
@Getter
public enum PrivacyTypeEnum {
  /** 自定义(此项需设置脱敏的范围)*/
  CUSTOMER,
  /** 姓名 */
  NAME,
  /** 身份证号 */
  ID_CARD,
  /** 手机号 */
  PHONE,
  /** 邮箱 */
  EMAIL,
}

2. Create the custom 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 the custom serializer: PrivacySerializer

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 com.zk.common.core.domain.enumerate.PrivacyTypeEnum;
import com.zk.common.core.utils.PrivacyUtil;
import java.io.IOException;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;

@NoArgsConstructor
@AllArgsConstructor
public class PrivacySerializer extends JsonSerializer
implements ContextualSerializer {

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

  @Override
  public void serialize(final String origin, final JsonGenerator jsonGenerator,
                        final SerializerProvider serializerProvider) throws IOException {
    if (StringUtils.isNotBlank(origin) && 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 (beanProperty != null) {
      if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
        PrivacyEncrypt privacyEncrypt = beanProperty.getAnnotation(PrivacyEncrypt.class);
        if (privacyEncrypt == null) {
          privacyEncrypt = beanProperty.getContextAnnotation(PrivacyEncrypt.class);
        }
        if (privacyEncrypt != null) {
          return new PrivacySerializer(privacyEncrypt.type(), privacyEncrypt.prefixNoMaskLen(),
              privacyEncrypt.suffixNoMaskLen(), privacyEncrypt.symbol());
        }
      }
      return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
    }
    return serializerProvider.findNullValueSerializer(null);
  }
}

4. Privacy utility class: PrivacyUtil

public class PrivacyUtil {

    /** 隐藏手机号中间四位 */
    public static String hidePhone(String phone) {
        return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    }

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

    /** 隐藏身份证 */
    public static String hideIDCard(String idCard) {
        return idCard.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1*****$2");
    }

    /** 【中文姓名】只显示第一个汉字,其他隐藏为星号,比如:任** */
    public static String hideChineseName(String chineseName) {
        if (chineseName == null) {
            return null;
        }
        return desValue(chineseName, 1, 0, "*");
    }

    /** 对字符串进行脱敏操作 */
    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();
    }

    public static void main(String[] args) {
        System.out.println(hideChineseName("张三三"));
    }
}

5. Annotation usage example

@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;
}

After adding @PrivacyEncrypt to the desired fields, the serialization process automatically masks the data, providing a clean and reusable solution for protecting sensitive user information.

Javabackend developmentCustom AnnotationJacksondata masking
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.