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

Spring AOP如何在日志方法接收参数前拦截并脱敏@Confidential标记的PII数据?

Spring AOP实现PII日志脱敏解决方案

问题核心分析

  1. 原切面拦截org.slf4j.Logger方法时,日志参数已经是POJO getter执行后的字符串,丢失了与@Confidential注解的关联,无法识别是否为PII数据。
  2. 切点中添加@annotation(Confidential)无效,因为该表达式匹配的是被调用方法本身携带该注解,而log.info()等日志方法并未标注@Confidential,因此切点无法匹配。

可行解决方案

方案1:用包装类标记PII数据(低侵入性)

通过自定义包装类标记需要脱敏的数据,切面识别该类后自动脱敏,无需修改POJO结构。

步骤1:创建PII包装类

public class PIIWrapper {
    private final String rawValue;

    public PIIWrapper(String rawValue) {
        this.rawValue = rawValue;
    }

    public String getRawValue() {
        return rawValue;
    }
}

步骤2:修改业务日志调用

将PII数据用PIIWrapper包裹后传入日志方法:

public void someMethod(PiiPojo pojo) {
    log.info("Passing in secret name: {}", new PIIWrapper(pojo.getSecretName()));
}

步骤3:调整切面逻辑

拦截自定义MyLogger的方法,识别包装类并执行脱敏:

@Aspect
public class PIILoggerAspect {
    // 拦截自定义Logger的所有方法,替换为实际包名
    @Around("execution(* com.example.MyLogger.*(..))")
    public Object maskPII(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof PIIWrapper) {
                // 自定义脱敏规则,例如保留最后2位,其余替换为***
                String maskedValue = "***" + ((PIIWrapper) args[i]).getRawValue()
                    .substring(Math.max(0, ((PIIWrapper) args[i]).getRawValue().length() - 2));
                args[i] = maskedValue;
            }
        }
        return joinPoint.proceed(args);
    }
}

方案2:基于反射解析POJO的PII字段(无需修改日志调用形式)

如果不想修改业务日志代码,可以直接传递POJO对象,切面通过反射识别@Confidential字段并脱敏。

步骤1:修改日志调用

直接传入POJO对象:

public void someMethod(PiiPojo pojo) {
    log.info("User info: {}", pojo);
}

步骤2:更新切面逻辑

通过反射遍历POJO字段,对带@Confidential注解的字段进行脱敏:

@Aspect
public class PIILoggerAspect {
    @Around("execution(* com.example.MyLogger.*(..))")
    public Object maskPII(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            if (args[i] != null) {
                args[i] = maskConfidentialFields(args[i]);
            }
        }
        return joinPoint.proceed(args);
    }

    private Object maskConfidentialFields(Object obj) {
        Class<?> clazz = obj.getClass();
        StringBuilder sb = new StringBuilder(clazz.getSimpleName()).append("[");
        
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            try {
                if (field.isAnnotationPresent(Confidential.class)) {
                    sb.append(field.getName()).append(":***,");
                } else {
                    sb.append(field.getName()).append(":").append(field.get(obj)).append(",");
                }
            } catch (IllegalAccessException e) {
                // 捕获反射异常,可根据需求添加日志或默认值
                sb.append(field.getName()).append(":UNKNOWN,");
            }
        }
        
        if (sb.length() > 1) {
            sb.deleteCharAt(sb.length() - 1);
        }
        sb.append("]");
        return sb.toString();
    }
}

方案3:栈跟踪识别PII方法调用(不推荐,性能较低)

如果坚持传递字符串参数,可以通过栈跟踪追溯参数来源是否为带@Confidential的getter方法,但该方式性能开销较大,仅适合低并发场景。

@Aspect
public class PIILoggerAspect {
    @Around("execution(* com.example.MyLogger.*(..))")
    public Object maskPII(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        
        // 遍历栈帧,跳过当前切面和日志方法,查找业务方法
        for (int i = 2; i < stackTrace.length; i++) {
            StackTraceElement element = stackTrace[i];
            try {
                Class<?> clazz = Class.forName(element.getClassName());
                Method method = clazz.getMethod(element.getMethodName());
                // 如果调用的方法带@Confidential注解,则脱敏当前日志参数
                if (method.isAnnotationPresent(Confidential.class)) {
                    for (int j = 0; j < args.length; j++) {
                        if (args[j] instanceof String) {
                            args[j] = "***" + ((String) args[j]).substring(Math.max(0, ((String) args[j]).length() - 2));
                        }
                    }
                    break;
                }
            } catch (Exception e) {
                // 忽略类/方法不存在的异常
                continue;
            }
        }
        return joinPoint.proceed(args);
    }
}

关键配置注意事项

  • 确保切点表达式拦截的是Spring管理的MyLogger Bean方法,而非org.slf4j.Logger接口,因为Spring AOP只能代理容器内的Bean。
  • 无需在切点中添加@annotation(Confidential),该表达式仅匹配被拦截方法本身带有注解的场景,与当前需求不符。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 21:14:52