Spring Boot 接口敏感字段脫敏的五大"王炸"方案
環境:SpringBoot3.4.2
1. 簡介
在實際項目中,經常會遇到一些敏感字段信息,像用戶的身份證號碼、銀行卡卡號、詳細家庭住址、手機號碼以及各類密碼等。若這些敏感字段在接口傳輸或展示過程中未作脫敏處理,一旦遭遇數據泄露事件,將給用戶帶來嚴重損失,包括財產被盜刷、個人隱私曝光,甚至可能引發身份盜用等風險。
接口敏感字段脫敏,通過對關鍵信息進行部分隱藏、替換或加密等操作,在不影響數據正常使用和分析的前提下,最大程度降低敏感信息暴露風險。
本篇文章我們將采用5種技術方案來實現敏感字段的脫敏。
2.實戰案例
準備環境
public class User {
private Long id ;
private String name ;
private Integer age ;
private String phone ;
private String idNo ;
// getters, setters, constructors
}
// Controller接口
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping
public ResponseEntity<User> query() {
return ResponseEntity.ok(new User(666L, "Pack_xg", 33,
"13399065789", "320311198512185678")) ;
}
}我們在編寫一個工具類,統一都由該工具類處理敏感字段
public class MaskUtils {
public static String maskString(String input) {
int length = input.length();
// 計算xxx、***、yyy的長度(總長度=xxx+***+yyy)
int xxxLength = length / 3; // 前1/3
int yyyLength = length / 3; // 后1/3
// 中間剩余部分用*
int starLength = length - xxxLength - yyyLength;
// 確保至少各保留1個字符(避免xxx或yyy為0)
xxxLength = Math.max(1, xxxLength);
yyyLength = Math.max(1, yyyLength);
starLength = Math.max(1, starLength);
// 重新調整(防止因取整導致總和超出原長度)
while (xxxLength + starLength + yyyLength > length) {
if (xxxLength > 1) xxxLength--;
else if (starLength > 1) starLength--;
else yyyLength--;
}
String xxx = input.substring(0, xxxLength);
String yyy = input.substring(length - yyyLength);
String stars = repeat("*", starLength);
return xxx + stars + yyy;
}
private static String repeat(String s, int count) {
return new String(new char[count]).replace("\0", s);
}
}2.1 自定義Json序列化
public class CommonMaskSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null || value.length() < 6) {
gen.writeString("***");
return;
}
String masked = MaskUtils.maskString(value) ;
gen.writeString(masked);
}
}接下來,修改實體對象如下:
public class User {
@JsonSerialize(using = CommonMaskSerializer.class)
private String phone ;
@JsonSerialize(using = CommonMaskSerializer.class)
private String idNo ;
}輸出結果

2.2 自定義Jackson模塊
上一種方法是字段級別的,雖然精準,但若多個實體都需要相同的邏輯,則會顯得冗余繁瑣。為了避免將所有內容都與注解綁定,我們可以通過自定義模塊方式注冊自定義序列化器,這些序列化器可應用于整個類型。如下代碼實現:
@Configuration
public class JacksonConfig {
@Bean
Jackson2ObjectMapperBuilderCustomizer maskingCustomizer() {
return builder -> builder.modules(new MaskingModule());
}
public static final class MaskingModule extends SimpleModule {
// 這里我們固定了處理那些字段名,你也可以將其放到配置文件中進行動態管理
private static final Set<String> fields = Set.of("idNo", "phone") ;
public MaskingModule() {
setSerializerModifier(new BeanSerializerModifier() {
@Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc,
List<BeanPropertyWriter> props) {
for (int i = 0; i < props.size(); i++) {
BeanPropertyWriter w = props.get(i);
if (w.getType().isTypeOrSubTypeOf(String.class) && fields.contains(w.getName())) {
props.set(i, new MaskingWriter(w));
}
}
return props;
}
});
}
}
static final class MaskingWriter extends BeanPropertyWriter {
private static final long serialVersionUID = 1L;
MaskingWriter(BeanPropertyWriter base) {
super(base);
}
@Override
public void serializeAsField(Object bean, JsonGenerator gen, com.fasterxml.jackson.databind.SerializerProvider prov)
throws Exception {
Object raw = get(bean);
if (raw == null) {
super.serializeAsField(bean, gen, prov);
return;
}
String masked = MaskUtils.maskString(raw.toString());
gen.writeStringField(getName(), masked);
}
}
}接下來,我們將實體類上的@JsonSerialize注解刪除,一樣能達到效果,并且此種方式不需要我們對實體類進行任何的修改,這能滿足絕大多數的場景。
圖片
2.3 使用AOP技術
利用AOP技術,在Controller接口返回后,通過反射技術對返回值進行處理。
自定義注解,只有返回值的類型上使用了該注解才進行處理
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Mask {
/**需要處理的字段*/
String[] value() default {} ;
}定義切面
@Aspect
@Component
public class MaskAspect {
private static final Map<Class<?>, List<PropMethod>> cache = new ConcurrentHashMap<>();
@AfterReturning(pointcut = "execution(* com.pack.sensitive.controller..*.*(..))", returning = "response")
public void applyMasking(Object response) throws Throwable {
Object target = response;
if (response instanceof ResponseEntity entity) {
target = entity.getBody();
}
Class<?> clazz = target.getClass();
Mask mask = clazz.getAnnotation(Mask.class);
if (mask == null) {
return;
}
List<String> fields = Arrays.asList(mask.value());
if (fields.isEmpty()) {
return ;
}
List<PropMethod> props = cache.get(clazz);
if (props == null) {
BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
if (fields.contains(pd.getName())) {
props = cache.computeIfAbsent(clazz, key -> new ArrayList<>()) ;
props.add(new PropMethod(pd.getReadMethod(), pd.getWriteMethod())) ;
}
}
}
if (props != null) {
for (PropMethod pm : props) {
pm.setter.invoke(target, MaskUtils.maskString((String) pm.getter.invoke(target))) ;
}
}
}
static record PropMethod(Method getter, Method setter) {
}
}修改User實體對象如下
@Mask({"phone", "idNo"})
public class User {
private Long id ;
private String name ;
private Integer age ;
private String phone ;
private String idNo ;
}調用Controller接口
圖片
2.4 使用ResponseBodyAdvice
我們可以利用Spring MVC的核心組件 ResponseBodyAdvice,該組件能夠在控制器方法返回的響應體被序列化輸出前,對返回數據進行攔截和處理。此種方式不是AOP技術,這里就完全使用反射技術實現脫敏。
在該方案中,我們還是利用Mask注解來進行標記處理,整體的處理與AOP基本相同。
@RestControllerAdvice
public class MaskBodyAdvice implements ResponseBodyAdvice<Object> {
private static final Map<Class<?>, List<PropMethod>> cache = new ConcurrentHashMap<>();
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true ;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
if (body == null) {
return null;
}
if (body instanceof ResponseEntity entity) {
body = entity.getBody() ;
}
if (body instanceof Collection<?> coll) {
for (Object o : coll) {
try {
maskField(o);
} catch (Throwable e) {}
}
return body;
}
try {
maskField(body);
} catch (Throwable e) {}
return body;
}
private void maskField(Object target) throws Throwable {
// 這里的處理邏輯與上面的AOP一樣
}
}此種方案核心與AOP一樣,只是這里不需要代理技術而已。
2.5 使用@JsonFilter
我們還可以配置 Jackson 屬性過濾器,也就是在需要處理的類上添加 @JsonFilter 注解,這樣我們就不需要為每個對象單獨添加注解的字段級過濾。
首先,修改實體對象
@JsonFilter("maskFilter")
public class User {
// ...
}這里的名稱 maskFilter 隨意。
接下來,自定義ObjectMapper
@Configuration
public class MaskFilterConfig {
private static final Set<String> fields = Set.of("idNo", "phone");
@Bean
Jackson2ObjectMapperBuilderCustomizer maskingFilterCustomizer() {
return builder -> {
SimpleFilterProvider filters = new SimpleFilterProvider().addFilter("maskFilter", new SimpleBeanPropertyFilter() {
@Override
public void serializeAsField(Object pojo, JsonGenerator gen, SerializerProvider prov, PropertyWriter writer)
throws Exception {
if (fields.contains(writer.getName())) {
Object val = (writer instanceof BeanPropertyWriter bpw) ? bpw.get(pojo) : null;
if (val == null) {
writer.serializeAsField(pojo, gen, prov);
return;
}
gen.writeStringField(writer.getName(), MaskUtils.maskString(val.toString()));
return;
}
writer.serializeAsField(pojo, gen, prov);
}
});
builder.filters(filters);
};
}
}通過上面的方法也能也能達到對敏感字段的處理。






























