如何避免WebFlux异常追踪中记录含敏感信息的Checkpoint
问题背景
使用DefaultWebClient调用下游API时,URI路径包含敏感数据。异常场景下WebFlux会将下游URI作为追踪信息的一部分,以WARN级别日志输出。现有约束:
- 禁止关闭WARN级别日志
- 无法重写DefaultWebClient.java、Mono.java或WebClientUtils.java
- 当前使用WebFlux 3.3.7版本,已尝试
checkpoint()、checkpoint("test checkpoint")、hide()、log("abc", Level.SEVERE)等方法,仍能看到包含敏感URI的checkpoint日志。
调用代码示例:
public Mono<DataResponse> callDownstream(String sensitiveId) { return webClient.get() .uri("URI-PATH", sensitiveId) .retrieve() .onStatus(HttpStatusCode::isError, resp -> resp.bodyToMono(ErrorDto.class).map(this::httpError)) .bodyToMono(DataResponse.class) .doFinally(signalType -> log.info(String.format("message=\"Finished calling downstream\""))); } private Throwable httpError(ErrorDto errorDto) { log.error("message=\"Error in calling downstream\""); return new CustomException("Error in calling downstream"); }
异常日志片段示例:
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ 500 INTERNAL_SERVER_ERROR from GET https://example.com:8081/my-service/manage/*SENSITIVE-DATA [DefaultWebClient]
...(后续堆栈信息略)
可行解决方案
1. 自定义ExchangeFilterFunction脱敏URI
通过添加ExchangeFilterFunction,在请求链路中替换日志展示用的URI(实际请求仍使用原URI),让DefaultWebClient生成的checkpoint使用脱敏后的URI:
// 构建脱敏后的WebClient实例 WebClient maskedWebClient = webClient.mutate() .filter((request, next) -> { // 根据实际URI结构编写脱敏逻辑 String originalUri = request.url().toString(); String maskedUri = originalUri.replaceAll("/manage/([^/]+)", "/manage/***"); // 创建包装后的请求,使用脱敏URI ClientRequest maskedRequest = ClientRequest.from(request) .url(URI.create(maskedUri)) .build(); return next.exchange(maskedRequest); }) .build();
核心逻辑:DefaultWebClient的checkpoint信息基于ClientRequest的url()生成,替换后checkpoint会输出脱敏后的内容。
2. 异常包装+hide()清除追踪信息
通过onErrorMap包装自定义异常,避免触发FluxOnAssembly的敏感URI追踪,结合hide()隐藏操作符链路:
public Mono<DataResponse> callDownstream(String sensitiveId) { return webClient.get() .uri("URI-PATH", sensitiveId) .retrieve() .onStatus(HttpStatusCode::isError, resp -> resp.bodyToMono(ErrorDto.class).map(this::httpError)) .bodyToMono(DataResponse.class) .doFinally(signalType -> log.info("message=\"Finished calling downstream\"")) .onErrorMap(ex -> { if (ex instanceof CustomException) { // 包装异常,跳过栈填充避免Assembly追踪 return new CustomException(ex.getMessage(), ex.getCause()) { @Override public Throwable fillInStackTrace() { return this; } }; } return ex; }) .hide(); }
注意:此方式会减少部分调试链路信息,但能有效避免敏感URI泄露。
3. 日志框架层面脱敏输出
针对Logback/Log4j2等日志框架,自定义转换器在日志输出环节直接清洗敏感内容:
以Logback为例,自定义脱敏转换器:
public class MaskingConverter extends ClassicConverter { @Override public String convert(ILoggingEvent event) { String message = event.getFormattedMessage(); // 根据实际URI规则替换敏感部分 return message.replaceAll("GET https://example.com:8081/my-service/manage/([^/]+)", "GET https://example.com:8081/my-service/manage/***"); } }
在logback.xml中配置使用该转换器:
<conversionRule conversionWord="maskedMsg" converterClass="com.yourpackage.MaskingConverter"/> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %maskedMsg%n</pattern> </encoder> </appender>
核心逻辑:在日志最终输出环节拦截并清洗敏感内容,从根源避免敏感数据落地。
内容的提问来源于stack exchange,提问作者aravind m

