You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何利用Spring AOP在Java DTO创建时对指定字段进行脱敏?

解决方案

首先明确:Spring AOP无法直接拦截非Spring Bean的构造器调用,而DTO(比如你用@Builder生成的User实例)通常不是Spring托管的Bean,所以拦截构造器的思路不可行。更合理的方案是在DTO实例化完成后、返回给前端前的环节进行脱敏,确保最终API返回的是脱敏数据,同时保留数据库中的原始信息。

以下是两种可行的AOP实现方案:

方案一:拦截Service层返回DTO的方法

通过环绕通知拦截Service层中返回DTO的方法,对返回的实例进行脱敏处理,适用于大多数业务场景。

完整Aspect代码

@Aspect
@Component
public class PersonalInfoAspect {

    // 切点:拦截com.example.service包下所有方法的返回值
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethodPointcut() {}

    // 环绕通知:处理返回的DTO脱敏
    @Around("serviceMethodPointcut()")
    public Object desensitizeDto(ProceedingJoinPoint joinPoint) throws Throwable {
        // 执行原方法,获取返回的DTO实例
        Object result = joinPoint.proceed();
        if (result == null) {
            return null;
        }
        // 对带有@PersonalInfo注解的字段进行脱敏
        processDesensitization(result);
        return result;
    }

    // 反射遍历字段并执行脱敏逻辑
    private void processDesensitization(Object obj) throws IllegalAccessException {
        Class<?> clazz = obj.getClass();
        Field[] fields = clazz.getDeclaredFields();
        
        for (Field field : fields) {
            if (field.isAnnotationPresent(PersonalInfo.class)) {
                field.setAccessible(true);
                Object fieldValue = field.get(obj);
                
                if (fieldValue instanceof String) {
                    String originalValue = (String) fieldValue;
                    // 根据字段类型执行对应脱敏逻辑,这里以姓名为例
                    String desensitizedValue = desensitizeName(originalValue);
                    field.set(obj, desensitizedValue);
                }
            }
        }
    }

    // 姓名脱敏逻辑:保留首字,其余替换为*
    private String desensitizeName(String name) {
        if (name == null || name.length() <= 1) {
            return name;
        }
        return name.substring(0, 1) + "*".repeat(name.length() - 1);
    }
}

方案二:拦截Builder的build()方法

如果你习惯用@Builder模式创建DTO,可以直接拦截Builder的build()方法,在实例生成后立即脱敏。

对应Aspect代码片段

@Aspect
@Component
public class PersonalInfoAspect {

    // 切点:拦截User.Builder的build()方法
    @Pointcut("execution(* com.example.dto.User$UserBuilder.build(..))")
    public void userBuilderBuildPointcut() {}

    @Around("userBuilderBuildPointcut()")
    public Object desensitizeFromBuilder(ProceedingJoinPoint joinPoint) throws Throwable {
        // 执行build()获取User实例
        User user = (User) joinPoint.proceed();
        // 执行脱敏处理
        processDesensitization(user);
        return user;
    }

    // 复用方案一中的processDesensitization和脱敏逻辑方法
    private void processDesensitization(Object obj) throws IllegalAccessException {
        // ... 同方案一代码
    }

    private String desensitizeName(String name) {
        // ... 同方案一代码
    }
}

扩展优化:支持多类型脱敏

如果需要对不同类型的个人信息(如手机号、邮箱)执行不同脱敏逻辑,可以扩展@PersonalInfo注解,添加脱敏类型属性:

扩展后的注解

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PersonalInfo {
    DesensitizeType type() default DesensitizeType.NAME;

    enum DesensitizeType {
        NAME, PHONE, EMAIL
    }
}

优化后的脱敏逻辑

private void processDesensitization(Object obj) throws IllegalAccessException {
    Class<?> clazz = obj.getClass();
    Field[] fields = clazz.getDeclaredFields();
    
    for (Field field : fields) {
        if (field.isAnnotationPresent(PersonalInfo.class)) {
            field.setAccessible(true);
            Object fieldValue = field.get(obj);
            if (fieldValue instanceof String) {
                String original = (String) fieldValue;
                PersonalInfo annotation = field.getAnnotation(PersonalInfo.class);
                String desensitized = switch (annotation.type()) {
                    case NAME -> desensitizeName(original);
                    case PHONE -> desensitizePhone(original);
                    case EMAIL -> desensitizeEmail(original);
                };
                field.set(obj, desensitized);
            }
        }
    }
}

// 手机号脱敏:保留前3位和后4位
private String desensitizePhone(String phone) {
    if (phone == null || phone.length() != 11) {
        return phone;
    }
    return phone.substring(0, 3) + "****" + phone.substring(7);
}

// 邮箱脱敏:保留前缀首字和域名
private String desensitizeEmail(String email) {
    if (email == null || !email.contains("@")) {
        return email;
    }
    String[] parts = email.split("@");
    if (parts[0].length() <= 1) {
        return email;
    }
    return parts[0].substring(0, 1) + "****@" + parts[1];
}

关键说明

  • 无需拦截构造器:Spring AOP的动态代理机制仅支持拦截Spring Bean的方法调用,非Bean的构造器无法被拦截,因此选择在DTO实例化完成后的环节处理更可靠。
  • 数据库数据不受影响:脱敏仅发生在内存中的DTO实例上,不会修改数据库中存储的原始数据,符合你的需求。

内容的提问来源于stack exchange,提问作者kaven

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 15:30:52