How to Automatically Mask Sensitive Data in Spring Boot with Jackson Annotations
This article explains how to implement unified data desensitization in Java Spring Boot applications by creating custom annotations, enums, serializers, and utility methods that automatically mask personal information during JSON serialization, complete with code examples and test results.
Introduction
In real business development we often need to mask users' private data, which means obscuring the data to protect privacy. For example, phone numbers or addresses can be hidden using asterisks (*) to avoid leaking personal information.
If the range of data to be masked is very small, such as a single field, a simple hide method can be used, which is easy to maintain. However, when many fields across the project need masking, a uniform approach at the data output stage becomes necessary.
Considering data output, many developers think of JSON serialization. The question is how to perform data masking during serialization.
Implementation
2.1 Add Dependency Packages
If the project already includes
spring-webor
spring-boot-starter-web, Jackson is already on the classpath and no extra dependency is needed. Otherwise, add the following Jackson dependencies:
<code><!--jackson dependency-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency></code>2.2 Define Sensitive Types Enum
<code>public enum SensitiveEnum {
CHINESE_NAME,
ID_CARD,
FIXED_PHONE,
MOBILE_PHONE,
ADDRESS,
EMAIL,
BANK_CARD,
CNAPS_CODE
}</code>2.3 Create Masking Annotation
<code>import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveSerialize.class)
public @interface SensitiveWrapped {
SensitiveEnum value();
}</code>2.4 Implement Custom JsonSerializer
<code>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;
import java.util.Objects;
public class SensitiveSerialize extends JsonSerializer<String> implements ContextualSerializer {
private SensitiveEnum type;
@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
switch (this.type) {
case CHINESE_NAME: jsonGenerator.writeString(SensitiveInfoUtils.chineseName(s)); break;
case ID_CARD: jsonGenerator.writeString(SensitiveInfoUtils.idCardNum(s)); break;
case FIXED_PHONE: jsonGenerator.writeString(SensitiveInfoUtils.fixedPhone(s)); break;
case MOBILE_PHONE: jsonGenerator.writeString(SensitiveInfoUtils.mobilePhone(s)); break;
case ADDRESS: jsonGenerator.writeString(SensitiveInfoUtils.address(s, 4)); break;
case EMAIL: jsonGenerator.writeString(SensitiveInfoUtils.email(s)); break;
case BANK_CARD: jsonGenerator.writeString(SensitiveInfoUtils.bankCard(s)); break;
case CNAPS_CODE: jsonGenerator.writeString(SensitiveInfoUtils.cnapsCode(s)); break;
}
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null && Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
SensitiveWrapped wrapped = beanProperty.getAnnotation(SensitiveWrapped.class);
if (wrapped == null) {
wrapped = beanProperty.getContextAnnotation(SensitiveWrapped.class);
}
if (wrapped != null) {
return new SensitiveSerialize(wrapped.value());
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
return serializerProvider.findNullValueSerializer(beanProperty);
}
public SensitiveSerialize() {}
public SensitiveSerialize(final SensitiveEnum type) { this.type = type; }
}
</code>2.5 Build Utility Class for Masking Logic
<code>import org.apache.commons.lang3.StringUtils;
public class SensitiveInfoUtils {
public static String chineseName(final String fullName) {
if (StringUtils.isBlank(fullName)) return "";
String name = StringUtils.left(fullName, 1);
return StringUtils.rightPad(name, StringUtils.length(fullName), "*");
}
public static String idCardNum(final String id) {
if (StringUtils.isBlank(id)) return "";
return StringUtils.left(id, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(id, 4), StringUtils.length(id), "*"), "***"));
}
public static String fixedPhone(final String num) {
if (StringUtils.isBlank(num)) return "";
return StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*");
}
public static String mobilePhone(final String num) {
if (StringUtils.isBlank(num)) return "";
return StringUtils.left(num, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*"), "***"));
}
public static String address(final String address, final int sensitiveSize) {
if (StringUtils.isBlank(address)) return "";
int length = StringUtils.length(address);
return StringUtils.rightPad(StringUtils.left(address, length - sensitiveSize), length, "*");
}
public static String email(final String email) {
if (StringUtils.isBlank(email)) return "";
int index = StringUtils.indexOf(email, "@");
if (index <= 1) return email;
return StringUtils.rightPad(StringUtils.left(email, 1), index, "*").concat(StringUtils.mid(email, index, StringUtils.length(email)));
}
public static String bankCard(final String cardNum) {
if (StringUtils.isBlank(cardNum)) return "";
return StringUtils.left(cardNum, 6).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(cardNum, 4), StringUtils.length(cardNum), "*"), "******"));
}
public static String cnapsCode(final String code) {
if (StringUtils.isBlank(code)) return "";
return StringUtils.rightPad(StringUtils.left(code, 2), StringUtils.length(code), "*");
}
}
</code>2.6 Test Entity Class
<code>public class UserEntity {
private Long userId;
private String name;
@SensitiveWrapped(SensitiveEnum.MOBILE_PHONE)
private String mobile;
@SensitiveWrapped(SensitiveEnum.ID_CARD)
private String idCard;
private String sex;
private int age;
// getters and setters omitted
}</code>2.7 Demo Program
<code>public class SensitiveDemo {
public static void main(String[] args) throws JsonProcessingException {
UserEntity userEntity = new UserEntity();
userEntity.setUserId(1L);
userEntity.setName("张三");
userEntity.setMobile("18000000001");
userEntity.setIdCard("420117200001011000008888");
userEntity.setAge(20);
userEntity.setSex("男");
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writeValueAsString(userEntity));
}
}
</code>Running the demo prints a JSON string where the mobile number and ID card are masked, e.g.:
<code>{"userId":1,"name":"张三","mobile":"180****0001","idCard":"420*****************8888","sex":"男","age":20}</code>If the project uses Spring MVC, the framework will automatically serialize the response object with Jackson, applying the same masking logic.
<code>@RequestMapping("/hello")
public UserEntity hello() {
UserEntity userEntity = new UserEntity();
// set fields as above
return userEntity;
}
</code>Accessing
http://127.0.0.1:8080/helloreturns the masked JSON.
Conclusion
Using annotation‑based global data masking in real business scenarios can effectively prevent sensitive data leakage. This article provided a practical, code‑first guide to implementing data desensitization in Java Spring applications.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.