Java案例怎么实现展示脱敏?完整实现方案与常见问题解析
目录导读
- 什么是数据脱敏?为什么需要展示脱敏?
- Java实现展示脱敏的三种主流方案
- 基于注解的AOP脱敏实现
- 自定义序列化脱敏(Jackson/JSON)
- 前端脱敏 vs 后端脱敏,哪个更优?
- 关键代码实现与性能对比
- 高频问题解答(FAQ)
什么是数据脱敏?为什么需要展示脱敏?
在Web应用、API接口或报表系统中,经常需要展示用户的敏感信息,如手机号、身份证号、银行卡号等。展示脱敏是指在不改变原始存储数据的前提下,对外展示时使用掩码(如 138****1234)替换真实内容。

核心原则:存储完整,展示隐藏。
常见脱敏场景包括:用户管理后台、订单详情页、日志打印、数据传输等。
Java实现展示脱敏的三种主流方案
| 方案 | 优点 | 缺点 |
|---|---|---|
| AOP + 注解 | 无侵入、统一管理 | 需要理解AOP与反射 |
| Jackson序列化过滤器 | 天然适配JSON输出 | 仅适用于JSON场景 |
| 手动工具类替换 | 简单直接 | 代码重复、难以维护 |
大多数企业项目选择 注解 + AOP 作为核心方案,因为其耦合度低,且易于扩展。
基于注解的AOP脱敏实现(推荐)
步骤1:定义脱敏注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
SensitiveType value() default SensitiveType.MOBILE;
}
步骤2:定义脱敏类型枚举与工具类
public enum SensitiveType {
MOBILE, // 手机号:138****1234
ID_CARD, // 身份证:110101********1234
BANK_CARD, // 银行卡:6222****1234
NAME // 姓名:张**
}
public class SensitiveUtil {
public static String mask(String value, SensitiveType type) {
if (value == null) return null;
switch (type) {
case MOBILE:
return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
case ID_CARD:
return value.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
case NAME:
if (value.length() == 1) return "*";
return value.charAt(0) + "**";
default: return value;
}
}
}
步骤3:编写AOP切面(Spring Boot)
@Aspect
@Component
public class SensitiveAspect {
@Around("@annotation(com.demo.annotation.Sensitive)")
public Object handle(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = joinPoint.proceed();
// 对返回结果中标记@Sensitive的字段进行脱敏
if (result instanceof Collection) {
((Collection<?>) result).forEach(this::maskField);
} else {
maskField(result);
}
return result;
}
private void maskField(Object obj) {
if (obj == null) return;
for (Field field : obj.getClass().getDeclaredFields()) {
field.setAccessible(true);
Sensitive sensitive = field.getAnnotation(Sensitive.class);
if (sensitive != null) {
try {
String original = (String) field.get(obj);
field.set(obj, SensitiveUtil.mask(original, sensitive.value()));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
关键点:AOP切面拦截Controller返回,利用反射修改字段值,注意性能:反射在低频场景下无影响,高频场景建议使用缓存字段元信息。
基于Jackson序列化脱敏(适合REST API)
如果项目使用Spring Boot默认的Jackson,可以通过自定义序列化器实现脱敏:
@JsonSerialize(using = MobileSerializer.class)
private String mobile;
public class MobileSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"));
}
}
优点:不依赖AOP,轻量级。
缺点:每一个字段都需要单独指定序列化器,代码重复。
前端脱敏 vs 后端脱敏,谁更安全?
| 维度 | 后端脱敏 | 前端脱敏 |
|---|---|---|
| 安全性 | 高:后端直接屏蔽 | 低:数据暴露在网络传输中 |
| 维护性 | 集中管理 | 前端各组件独立处理 |
| 灵活性 | 需要修改后端代码 | 可动态控制显示 |
对于金融、医疗等合规要求高的系统,必须采用后端脱敏。
高频问题解答(FAQ)
Q1:脱敏后的数据如何排序或搜索?
A:搜索和逻辑处理一定要基于原始字段,脱敏仅仅影响展示层,可以在数据库中增加冗余字段,或使用 @Transient 在实体中新增脱敏字段。
Q2:脱敏会影响单元测试吗?
A:会,建议在测试中关闭AOP切面(通过 @ActiveProfiles("test") 配置排除),或直接测试原始字段。
Q3:如果字段值为空或非字符串怎么办?
A:工具类需要增加判空逻辑,并且非字符串字段(如 Integer)不应使用脱敏注解,可以在注解中增加 @Target(ElementType.FIELD) + 类型校验。
Q4:多语言环境下脱敏规则是否一致?
A:脱敏规则和语言无关,但姓名脱敏(如中文"张三" -> "张")与英文("John" -> "J")规则不同,建议按语言扩展 SensitiveUtil。
Q5:能否让脱敏规则动态可配置?
A:可以,将规则存储在数据库或配置中心(如Nacos),在 SensitiveUtil 中动态读取,实现热更新。
总结与最佳实践
- 推荐方案:注解 + AOP,统一管理、易于扩展。
- 性能优化:对高频字段使用
ConcurrentHashMap缓存字段与注解信息,减少反射次数。 - 安全红线:不要在前端存储或传输脱敏前的敏感数据,后端AOP是最后的防线。
- 日志脱敏:同样可以使用AOP拦截日志打印语句,使用
logback或log4j2的MessageConverter实现。
通过本文的完整案例,你可以快速在企业级Java项目中落地展示脱敏功能,同时满足合规与安全要求。