Java案例如何实现数据脱敏?

wen python案例 4

本文目录导读:

Java案例如何实现数据脱敏?

  1. 核心思路
  2. 方案一:基于Jackson序列化注解(最推荐,适用于JSON输出)
  3. 方案二:基于AOP切面(适用于Service层或非JSON场景)
  4. 方案三:简单工具类硬编码(适用于少量场景)
  5. 方案四:MyBatis / MyBatis-Plus 拦截器
  6. 方案五:日志脱敏(Logback框架)
  7. 最佳实践建议

在Java中实现数据脱敏,通常有几种主流的方案,从简单的硬编码替换到基于注解的框架级实现,再到利用数据库本身的脱敏功能。

下面我将介绍几种最实用、最常用的实现方式,并重点给出基于 注解 + AOPJackson序列化 的高扩展性方案。

核心思路

无论哪种方案,核心逻辑都是:在数据展示(输出)时,对特定字段进行替换/遮盖,而不是修改原始数据。


基于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实现。

  1. 定义转换器,正则匹配手机号、身份证等。
  2. logback-spring.xml配置:
<conversionRule conversionWord="msg" converterClass="com.example.log.DesensitizeConverter"/>

最佳实践建议

场景 推荐方案
REST API返回JSON Jackson注解 + 自定义序列化器(最优雅,性能好)
Service层或非JSON输出 AOP
临时、简单跑批任务 工具类硬编码
企业级统一治理 方案一 + 方案二组合

最典型的企业级做法:在VO(视图对象)级别的字段上加@Desensitized注解,Jackson自动序列化,这样业务层、持久层永远操作原始数据,只有在“出网关”(Controller返回)时才脱敏,安全且干净。

抱歉,评论功能暂时关闭!