前言

个人敏感信息保护已成为合规与品牌信任的底线需求。数据脱敏并非简单“打星号”, 而是从“产生、传输、存储、展示、日志、审计”全链路的系统化工程。本文给出一套在 SpringBoot 中可落地的通用方案, 兼顾可配置性、性能与安全边界。

一、敏感数据分级与合规约束

  • P0 核心敏感: 身份证号、银行卡号、密码、密钥、私密地址
  • P1 高敏: 手机号、邮箱、姓名、住址、IP、设备号
  • P2 业务敏感: 订单号、用户ID、内部单号

分级决定脱敏强度、记录审计策略与访问控制边界, 并影响是否需要加密存储与密钥轮换。

二、全链路脱敏范围与策略

  • 接口入参校验与清洗
  • 应用层返回值序列化脱敏
  • 持久化读写层: ORM/Mapper 拦截
  • 日志打印与埋点脱敏
  • 导出报表与消息队列载荷脱敏

三、注解 + AOP 执行时脱敏设计

核心思路: 使用注解标注敏感字段或方法返回值, 由 AOP 统一处理。支持按“规则类型 + 掩码参数”实现可配置。

3.1 注解定义

1
2
3
4
5
6
7
8
9
10
11
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
public @interface Sensitive {
SensitiveRule rule();
int prefix() default 0; // 可选: 保留前缀位数
int suffix() default 0; // 可选: 保留后缀位数
}

public enum SensitiveRule {
MOBILE, ID_CARD, BANK_CARD, EMAIL, NAME, ADDRESS, CUSTOM
}

3.2 规则与工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class MaskingUtils {
private MaskingUtils() {}

public static String maskMobile(String s) {
if (s == null || s.length() < 7) return s;
return s.substring(0,3) + "****" + s.substring(s.length()-4);
}

public static String maskEmail(String s) {
if (s == null) return null;
int i = s.indexOf('@');
if (i <= 1) return "*" + s.substring(i);
return s.charAt(0) + "****" + s.substring(i);
}
}

3.3 切面处理返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Aspect
@Component
public class SensitiveAspect {
@Around("execution(* com.example..controller..*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object ret = pjp.proceed();
return maskObject(ret);
}

private Object maskObject(Object obj) {
if (obj == null) return null;
if (obj instanceof Collection<?> col) {
col.forEach(this::maskObject);
return obj;
}
if (obj.getClass().getPackageName().startsWith("java.")) return obj;
for (Field f : obj.getClass().getDeclaredFields()) {
Sensitive ann = f.getAnnotation(Sensitive.class);
if (ann == null) continue;
f.setAccessible(true);
Object v = ReflectionUtils.getField(f, obj);
if (v instanceof String s) {
String masked = applyRule(ann, s);
ReflectionUtils.setField(f, obj, masked);
}
}
return obj;
}

private String applyRule(Sensitive ann, String s) {
return switch (ann.rule()) {
case MOBILE -> MaskingUtils.maskMobile(s);
case EMAIL -> MaskingUtils.maskEmail(s);
default -> genericMask(s, ann.prefix(), ann.suffix());
};
}

private String genericMask(String s, int prefix, int suffix) {
if (s == null) return null;
int len = s.length();
int p = Math.min(prefix, len);
int q = Math.min(suffix, Math.max(0, len - p));
int stars = Math.max(0, len - p - q);
return s.substring(0, p) + "*".repeat(stars) + s.substring(len - q);
}
}

适用: 控制器返回 DTO 场景, 快速落地, 与序列化脱敏可组合。

四、序列化层脱敏 Jackson 模块

在 JSON 输出层统一处理, 对标注 @Sensitive 的字段应用 JsonSerializer。优势是与 AOP 解耦, 仅在序列化时生效, 不污染内存对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class SensitiveSerializer extends JsonSerializer<String> {
private final SensitiveRule rule;
private final int prefix;
private final int suffix;

public SensitiveSerializer(SensitiveRule rule, int prefix, int suffix) {
this.rule = rule; this.prefix = prefix; this.suffix = suffix;
}

@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
String masked = switch (rule) {
case MOBILE -> MaskingUtils.maskMobile(value);
case EMAIL -> MaskingUtils.maskEmail(value);
default -> genericMask(value, prefix, suffix);
};
gen.writeString(masked);
}

private String genericMask(String s, int pre, int suf) {
if (s == null) return null;
int len = s.length();
int p = Math.min(pre, len);
int q = Math.min(suf, Math.max(0, len - p));
int stars = Math.max(0, len - p - q);
return s.substring(0, p) + "*".repeat(stars) + s.substring(len - q);
}
}
1
2
3
4
5
6
7
8
9
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveSerializer.class)
public @interface JsonSensitive {
SensitiveRule rule();
int prefix() default 0;
int suffix() default 0;
}

注册模块:

1
2
3
4
5
6
7
8
9
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// 可注册模块扩展或全局 mixin
return mapper;
}
}

五、持久化与查询层脱敏

  • 写入前: 对需要密文存储的数据进行加密(如 Jasypt/KMS), 严禁明文入库
  • 查询后: 仅在出库面向外部场景进行脱敏展示, 内部闭环计算保留原值
  • MyBatis 插件/拦截器: 对 ResultSet 到对象映射阶段注入脱敏逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
public class DesensitizeInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
if (result instanceof List<?> list) {
for (Object row : list) maskByAnnotation(row);
}
return result;
}

private void maskByAnnotation(Object obj) {
for (Field f : obj.getClass().getDeclaredFields()) {
Sensitive ann = f.getAnnotation(Sensitive.class);
if (ann == null) continue;
f.setAccessible(true);
Object v = ReflectionUtils.getField(f, obj);
if (v instanceof String s) {
String masked = s.length() > 0 ? "*" + s.substring(Math.max(1, s.length() - 1)) : s;
ReflectionUtils.setField(f, obj, masked);
}
}
}
}

六、日志与审计脱敏

  • 禁止将明文敏感信息写入日志
  • 使用 MaskingLayout 或 Logback TurboFilter 对日志参数进行脱敏
  • Trace/Span 标签仅记录哈希或截断值, 避免可逆还原

示例 Logback 过滤器思路:

1
<turboFilter class="com.example.log.MaskTurboFilter"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MaskTurboFilter extends TurboFilter {
@Override
public FilterReply decide(Marker marker, Logger logger, Level level,
String format, Object[] params, Throwable t) {
if (params != null) {
for (int i = 0; i < params.length; i++) {
if (params[i] instanceof String s) params[i] = sanitize(s);
}
}
return FilterReply.NEUTRAL;
}

private String sanitize(String s) {
return s.replaceAll("(1[3-9]\\d{9})", "***$1***");
}
}

七、规则引擎与可配置策略

  • 规则元数据: 字段名、规则类型、保留位数、场景(接口/日志/导出)
  • 热更新: 配置中心推送规则, 通过 AtomicReference/双缓冲生效
  • 多场景策略: 接口场景强脱敏, 日志场景严控输出, 内部计算场景可不脱敏

八、性能与稳定性

  • 序列化层脱敏优先, AOP 次之, 避免多次遍历
  • 热点对象避免反射开销: 缓存字段元信息或使用 ASM/MapStruct
  • 压测关注 P99 序列化耗时与 GC 回收压力

九、安全边界与绕过防护

  • 黑名单关键字与格式校验, 防止“看似非敏感”绕过
  • 对导出/下载等异步任务进行二次检查与最小化字段集
  • 与权限系统联动, 非授权主体无原值访问能力

十、实施清单

  1. 敏感数据分级与清单
  2. 注解/规则定义与覆盖率评估
  3. Jackson 序列化脱敏与单元测试
  4. MyBatis 拦截器/日志过滤器集成
  5. 性能基线与压测报告
  6. 审计与回溯机制
  7. 灰度与回滚预案

十一、FAQ

  • Q: 注解+AOP 与 Jackson 脱敏如何取舍

    • A: 面向外部输出优先序列化层; AOP 适合统一治理或非 JSON 输出场景
  • Q: 是否需要加密存储

    • A: P0/P1 推荐加密, 脱敏仅用于展示; 密钥需轮换与权限分层
  • Q: 如何避免误脱敏

    • A: 引入白名单字段与规则测试集, CI 中加入快照校验