在当今数据驱动的业务场景中,敏感信息保护已成为系统设计的核心需求之一。作为一名长期奋战在一线的Java开发者,我深刻体会到传统脱敏方案的局限性。本文将分享我在实际项目中设计的自定义脱敏方案,它不仅解决了Hutool等工具类存在的痛点,还通过注解+AOP+序列化的组合拳,实现了声明式、统一化的敏感数据处理。
这个方案源于我在金融支付系统的实战经验。当时我们需要处理用户身份证、银行卡、手机号等二十余类敏感字段,涉及接口返回、日志打印、数据导出等多种场景。最初采用Hutool工具类硬编码的方式,很快暴露出三个致命问题:一是开发人员容易遗漏脱敏调用;二是相同字段在不同场景下脱敏规则不一致;三是新需求如"保留前2后3位"等定制化规则难以扩展。
先看几个实际业务中的脱敏需求示例:
场景一:用户信息查询接口
json复制// 原始数据
{
"name": "张三丰",
"mobile": "13812345678",
"idCard": "110101199001011234"
}
// 期望返回
{
"name": "张*丰",
"mobile": "138****5678",
"idCard": "110***********1234"
}
场景二:交易日志记录
code复制原始日志:用户[13812345678]通过卡号[6225880123456789]转账100元
脱敏后:用户[138****5678]通过卡号[6225********6789]转账100元
场景三:数据导出Excel
code复制原始数据:欧阳锋,13900001111,上海市浦东新区张江高科技园区
脱敏后:欧**锋,139****1111,上海市浦东新区****
Hutool的DesensitizedUtil虽然提供了基础脱敏能力,但在实际企业级应用中存在明显不足:
侵入性强:每个需要脱敏的地方都要显式调用工具类,容易遗漏。我曾遇到过因为开发人员忘记调用脱敏方法,导致生产环境日志泄露用户手机号的严重事故。
维护困难:当业务方要求将手机号脱敏规则从"前3后4"改为"前2后3"时,需要全局搜索并修改所有相关代码,风险极高。
场景局限:仅支持字符串脱敏,无法处理对象嵌套场景。比如User对象中包含List
| 方案 | 实现复杂度 | 侵入性 | 多场景支持 | 规则扩展性 | 性能影响 |
|---|---|---|---|---|---|
| Hutool工具类 | 低 | 高 | 差 | 差 | 低 |
| 注解+Jackson序列化 | 中 | 低 | 中(仅JSON) | 好 | 中 |
| AOP动态处理 | 高 | 低 | 好 | 好 | 中 |
| 本方案(组合模式) | 高 | 低 | 极好 | 极好 | 中 |
经过综合评估,最终选择了注解+序列化+AOP的组合方案。这个方案虽然实现复杂度较高,但能完美满足以下核心需求:
整个方案的核心在于精心设计的注解体系:
java复制// 字段级注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {
// 脱敏类型枚举
SensitiveType type() default SensitiveType.CUSTOM;
// 自定义规则,如"3,4"表示保留前3后4位
String rule() default "";
// 是否忽略空值
boolean ignoreEmpty() default true;
}
// 方法级注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveResult {
// 是否深度处理(递归处理对象中的对象)
boolean deep() default true;
}
这里有几个设计亮点:
为了支持灵活的脱敏规则,我们采用策略模式设计脱敏算法:
java复制public interface SensitiveStrategy {
String desensitize(String origin, String rule);
}
// 手机号脱敏策略
public class MobileStrategy implements SensitiveStrategy {
@Override
public String desensitize(String origin, String rule) {
if (StringUtils.isBlank(origin)) return origin;
// 默认前3后4
int prefix = 3;
int suffix = 4;
// 解析自定义规则
if (StringUtils.isNotBlank(rule)) {
String[] parts = rule.split(",");
if (parts.length == 2) {
prefix = Integer.parseInt(parts[0]);
suffix = Integer.parseInt(parts[1]);
}
}
// 边界检查
if (origin.length() < prefix + suffix) {
return origin;
}
String prefixStr = origin.substring(0, prefix);
String suffixStr = origin.substring(origin.length() - suffix);
return prefixStr + StringUtils.repeat('*', origin.length() - prefix - suffix) + suffixStr;
}
}
策略工厂类负责管理所有策略实例:
java复制public class SensitiveStrategyFactory {
private static final Map<SensitiveType, SensitiveStrategy> STRATEGY_MAP = new EnumMap<>(SensitiveType.class);
static {
STRATEGY_MAP.put(SensitiveType.MOBILE_PHONE, new MobileStrategy());
STRATEGY_MAP.put(SensitiveType.ID_CARD, new IdCardStrategy());
// 其他策略注册...
}
public static SensitiveStrategy getStrategy(SensitiveType type) {
return STRATEGY_MAP.getOrDefault(type, new DefaultStrategy());
}
}
对于JSON返回场景,我们实现自定义序列化器:
java复制public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
private SensitiveType type;
private String rule;
private boolean ignoreEmpty;
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null || (ignoreEmpty && value.isEmpty())) {
gen.writeNull();
return;
}
SensitiveStrategy strategy = SensitiveStrategyFactory.getStrategy(type);
String result = strategy.desensitize(value, rule);
gen.writeString(result);
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
Sensitive annotation = property.getAnnotation(Sensitive.class);
if (annotation != null) {
this.type = annotation.type();
this.rule = annotation.rule();
this.ignoreEmpty = annotation.ignoreEmpty();
return this;
}
return prov.findValueSerializer(property.getType(), property);
}
}
关键点说明:
对于非JSON场景,我们通过AOP实现对象脱敏:
java复制@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 1) // 确保在事务切面之前执行
public class SensitiveAspect {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Around("@annotation(sensitiveResult)")
public Object aroundAdvice(ProceedingJoinPoint pjp, SensitiveResult sensitiveResult) throws Throwable {
Object result = pjp.proceed();
if (result == null) return null;
// 深度克隆对象,避免修改原始数据
Object cloned = objectMapper.readValue(objectMapper.writeValueAsString(result), result.getClass());
// 执行脱敏
SensitiveProcessor.process(cloned, sensitiveResult.deep());
return cloned;
}
}
脱敏处理器核心逻辑:
java复制public class SensitiveProcessor {
public static void process(Object target, boolean deep) {
if (target == null) return;
// 处理集合类型
if (target instanceof Collection) {
((Collection<?>) target).forEach(item -> process(item, deep));
return;
}
// 处理Map类型
if (target instanceof Map) {
((Map<?, ?>) target).values().forEach(value -> process(value, deep));
return;
}
// 处理普通对象
Class<?> clazz = target.getClass();
while (clazz != Object.class) {
for (Field field : clazz.getDeclaredFields()) {
processField(target, field, deep);
}
clazz = clazz.getSuperclass();
}
}
private static void processField(Object target, Field field, boolean deep) {
try {
field.setAccessible(true);
Object value = field.get(target);
if (value == null) return;
// 处理String类型字段
if (value instanceof String) {
Sensitive annotation = field.getAnnotation(Sensitive.class);
if (annotation != null) {
String desensitized = SensitiveStrategyFactory.getStrategy(annotation.type())
.desensitize((String) value, annotation.rule());
field.set(target, desensitized);
}
}
// 递归处理嵌套对象
else if (deep && !isJavaClass(value.getClass())) {
process(value, true);
}
} catch (Exception e) {
throw new SensitiveException("脱敏处理失败", e);
}
}
private static boolean isJavaClass(Class<?> clazz) {
return clazz.getClassLoader() == null;
}
}
方案支持多种形式的自定义规则:
实现示例:
java复制public class RegexStrategy implements SensitiveStrategy {
@Override
public String desensitize(String origin, String rule) {
if (StringUtils.isBlank(rule)) return origin;
try {
Pattern pattern = Pattern.compile(rule);
Matcher matcher = pattern.matcher(origin);
if (matcher.find()) {
return matcher.replaceAll("****");
}
return origin;
} catch (PatternSyntaxException e) {
log.warn("Invalid regex rule: {}", rule);
return origin;
}
}
}
优化后的处理逻辑:
java复制public class OptimizedSensitiveProcessor {
private static final ConcurrentMap<Field, Sensitive> FIELD_CACHE = new ConcurrentHashMap<>();
public static void process(Object target) {
// 优化后的处理逻辑...
}
}
xml复制<dependency>
<groupId>com.example</groupId>
<artifactId>sensitive-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
yaml复制sensitive:
strategies:
mobile:
pattern: "3,4"
idCard:
pattern: "1,1"
ignore-fields:
- com.example.dto.UserDTO.nickname
java复制@Configuration
@EnableAspectJAutoProxy
public class SensitiveAutoConfiguration {
@Bean
public SensitiveAspect sensitiveAspect() {
return new SensitiveAspect();
}
@Bean
public Module sensitiveModule() {
SimpleModule module = new SimpleModule();
module.addSerializer(String.class, new SensitiveJsonSerializer());
return module;
}
}
DTO定义:
java复制public class UserDTO {
@Sensitive(type = SensitiveType.NAME)
private String name;
@Sensitive(type = SensitiveType.MOBILE_PHONE)
private String mobile;
@Sensitive(type = SensitiveType.ADDRESS)
private String address;
// getters/setters...
}
Controller使用:
java复制@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
@SensitiveResult
public UserDTO getUser(@PathVariable Long id) {
// 查询用户信息
return userService.getUserById(id);
}
}
java复制public class SensitiveTest {
@Test
public void testMobileDesensitize() {
UserDTO user = new UserDTO();
user.setMobile("13812345678");
SensitiveProcessor.process(user);
assertEquals("138****5678", user.getMobile());
}
@Test
public void testJsonSerialize() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new SensitiveModule());
UserDTO user = new UserDTO();
user.setName("张三");
user.setMobile("13812345678");
String json = mapper.writeValueAsString(user);
assertTrue(json.contains("张*"));
assertTrue(json.contains("138****5678"));
}
}
问题现象:接口响应时间从50ms增加到200ms
排查步骤:
解决方案:
问题场景:
java复制public class OrderDTO {
private UserDTO user;
private List<OrderItem> items;
// ...
}
解决方案:
日期/数字脱敏:
java复制@Sensitive(type = SensitiveType.CUSTOM, rule = "yyyy-MM-dd")
private Date birthDate;
枚举值脱敏:
java复制public enum Gender {
@Sensitive(type = SensitiveType.CUSTOM, rule = "MALE->M,FEMALE->F")
MALE, FEMALE
}
| 维度 | Hutool方案 | 本方案 |
|---|---|---|
| 代码侵入性 | 高(需显式调用) | 低(注解声明) |
| 维护成本 | 高(散落在各处) | 低(集中管理) |
| 扩展性 | 差(需修改工具类) | 好(策略模式) |
| 多场景支持 | 仅支持字符串 | 支持对象/集合/嵌套 |
| 性能 | 高(直接调用) | 中(反射+序列化开销) |
ShardingSphere也提供了数据脱敏功能,但主要面向数据库层面。我们的方案更侧重应用层脱敏,两者可以结合使用:
经过多个项目的实践验证,我总结了以下经验:
注解设计原则:
性能优化建议:
团队协作规范:
监控指标:
这个方案目前已在公司内部多个核心系统上线,日均处理超过2000万次脱敏操作,有效降低了敏感数据泄露风险。最大的收获是:好的技术方案不仅要解决当前问题,更要为未来变化留出扩展空间。