本文目录导读:

- 核心思路
- 方案一:基于Jackson序列化注解(最推荐,适用于JSON输出)
- 方案二:基于AOP切面(适用于Service层或非JSON场景)
- 方案三:简单工具类硬编码(适用于少量场景)
- 方案四:MyBatis / MyBatis-Plus 拦截器
- 方案五:日志脱敏(Logback框架)
- 最佳实践建议
在Java中实现数据脱敏,通常有几种主流的方案,从简单的硬编码替换到基于注解的框架级实现,再到利用数据库本身的脱敏功能。
下面我将介绍几种最实用、最常用的实现方式,并重点给出基于 注解 + AOP 或 Jackson序列化 的高扩展性方案。
核心思路
无论哪种方案,核心逻辑都是:在数据展示(输出)时,对特定字段进行替换/遮盖,而不是修改原始数据。
基于Jackson序列化注解(最推荐,适用于JSON输出)
如果你使用Spring Boot,并且大多数数据脱敏发生在JSON序列化时,这种方式最为优雅。
定义脱敏策略类
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
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 enum DesensitizedType {
CHINESE_NAME, // 中文姓名:张**
ID_CARD, // 身份证:110110********1234
FIXED_PHONE, // 固定电话:010-****1234
MOBILE_PHONE, // 手机号:138****1234
ADDRESS, // 地址:北京市海淀区****
EMAIL, // 邮箱:s****@qq.com
BANK_CARD, // 银行卡号:6222****1234
PASSWORD // 密码:******
}
// 脱敏工具类
class DesensitizedUtils {
public static String desensitize(String value, DesensitizedType type) {
if (value == null || value.isEmpty()) return value;
switch (type) {
case CHINESE_NAME:
return value.replaceAll("(.).*", "$1**");
case MOBILE_PHONE:
return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
case ID_CARD:
return value.replaceAll("(\\d{6})\\d{10}(\\w{4})", "$1**********$2");
case EMAIL:
return value.replaceAll("(\\w?)(\\w+)(\\w)(@\\w+)", "$1****$3$4");
case PASSWORD:
return "******";
default:
return value;
}
}
}
创建自定义序列化器
public class DesensitizationSerializer extends JsonSerializer<String> implements ContextualSerializer {
private DesensitizedType type;
public DesensitizationSerializer() {}
public DesensitizationSerializer(DesensitizedType type) {
this.type = type;
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 在这里调用脱敏工具类
gen.writeString(DesensitizedUtils.desensitize(value, type));
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
if (property != null) {
// 读取字段上的自定义注解
Desensitized annotation = property.getAnnotation(Desensitized.class);
if (annotation != null) {
return new DesensitizationSerializer(annotation.type());
}
}
return prov.findValueSerializer(property.getType(), property);
}
}
定义注解
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.*;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DesensitizationSerializer.class)
public @interface Desensitized {
DesensitizedType type();
}
在实体类中使用
public class UserVO {
private Long id;
@Desensitized(type = DesensitizedType.CHINESE_NAME)
private String userName; // 输出时自动变为 张**
@Desensitized(type = DesensitizedType.MOBILE_PHONE)
private String phone; // 输出时自动变为 138****1234
@Desensitized(type = DesensitizedType.ID_CARD)
private String idCard; // 输出时自动脱敏
}
效果:调用 ObjectMapper.writeValueAsString(userVO) 或 Spring Boot 返回 @ResponseBody 时,字段会自动脱敏。
基于AOP切面(适用于Service层或非JSON场景)
如果脱敏逻辑需要在Service层执行,或者你需要返回非JSON数据(如HTML、PDF),可以使用AOP。
定义注解(同上,简化版)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DesensitizeResult {
}
编写AOP切面
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
@Aspect
@Component
public class DesensitizeAspect {
@Around("@annotation(com.example.annotation.DesensitizeResult)")
public Object desensitize(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = joinPoint.proceed();
// 对返回结果进行脱敏处理
if (result != null) {
processObject(result);
}
return result;
}
private void processObject(Object obj) {
// 这里进行递归遍历,查找带有 @Desensitized 注解的字段
// 逻辑类似于方案一,但手动处理
// 省略反射处理的详细代码...
}
}
缺点:需要写反射遍历逻辑,性能略低于Jackson方案。
简单工具类硬编码(适用于少量场景)
如果只是偶尔需要脱敏,或者没有使用Jackson、AOP的条件,可以最简单直接调用工具类。
// 在输出时调用 User user = userService.getUserById(1L); UserVO vo = new UserVO(); vo.setUserName(DesensitizedUtils.desensitize(user.getUserName(), DesensitizedType.CHINESE_NAME)); vo.setPhone(DesensitizedUtils.desensitize(user.getPhone(), DesensitizedType.MOBILE_PHONE));
缺点:代码冗余,耦合度高,维护困难。
MyBatis / MyBatis-Plus 拦截器
如果你希望在ORM层面直接处理(例如从数据库查询出来后就脱敏),可以配置MyBatis拦截器或MyBatis-Plus的字段填充器。
@Component
public class DesensitizateInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
// 对result进行脱敏处理
return result;
}
}
注意:这种方式会影响所有查询,如果只是特定场景需要原始数据,需要谨慎使用。
日志脱敏(Logback框架)
对于日志输出,可以通过Logback的MessageConverter或动态替换%msg实现。
- 定义转换器,正则匹配手机号、身份证等。
- 在
logback-spring.xml配置:
<conversionRule conversionWord="msg" converterClass="com.example.log.DesensitizeConverter"/>
最佳实践建议
| 场景 | 推荐方案 |
|---|---|
| REST API返回JSON | Jackson注解 + 自定义序列化器(最优雅,性能好) |
| Service层或非JSON输出 | AOP |
| 临时、简单跑批任务 | 工具类硬编码 |
| 企业级统一治理 | 方案一 + 方案二组合 |
最典型的企业级做法:在VO(视图对象)级别的字段上加@Desensitized注解,Jackson自动序列化,这样业务层、持久层永远操作原始数据,只有在“出网关”(Controller返回)时才脱敏,安全且干净。