Spring WebFlux中如何将Authentication用户名存入MDC增强日志
Spring Boot WebFlux 全局MDC注入用户名解决方案
问题根源
WebFlux基于Reactor响应式框架,而MDC依赖ThreadLocal存储数据。响应式流中线程会频繁切换,普通的MDC.put()只能在当前线程生效,线程切换后MDC数据丢失,导致reactor线程日志无user_id;而在Controller中设置MDC,又会错过初始请求阶段的日志。
解决方案思路
利用Reactor的Context传递用户名(Context跟随整个响应式流,不受线程切换影响),再通过Reactor Hook自动将Context中的用户名同步到MDC,确保所有线程执行日志时都能拿到该字段。
步骤1:编写WebFilter注入用户名到Reactor Context
import org.springframework.security.core.Authentication; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; import reactor.util.context.Context; @Component public class MdcWebFilter implements WebFilter { private static final String MDC_USER_ID_KEY = "user_id"; private static final String CONTEXT_USER_KEY = "USER_NAME"; @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { return ReactiveSecurityContextHolder.getContext() .map(securityContext -> securityContext.getAuthentication()) .defaultIfEmpty(null) .flatMap(authentication -> { // 处理匿名用户场景 String username = authentication != null ? authentication.getName() : "anonymous"; // 将用户名绑定到Reactor Context,跟随整个响应式流 return chain.filter(exchange) .contextWrite(Context.of(CONTEXT_USER_KEY, username)); }); } }
步骤2:配置Reactor Hook自动同步Context到MDC
import org.springframework.context.annotation.Configuration; import reactor.core.publisher.CoreSubscriber; import reactor.core.publisher.Hooks; import reactor.core.publisher.Subscription; import reactor.util.context.ContextView; import static org.slf4j.MDC.put; import static org.slf4j.MDC.remove; @Configuration public class ReactorMdcConfig { private static final String CONTEXT_USER_KEY = "USER_NAME"; private static final String MDC_USER_ID_KEY = "user_id"; public ReactorMdcConfig() { Hooks.onEachOperator((scannable, coreSubscriber) -> new CoreSubscriber<>() { @Override public void onSubscribe(Subscription s) { coreSubscriber.onSubscribe(s); } @Override public void onNext(Object o) { syncContextToMdc(coreSubscriber.currentContext()); try { coreSubscriber.onNext(o); } finally { // 清理MDC,避免线程复用导致数据污染 remove(MDC_USER_ID_KEY); } } @Override public void onError(Throwable t) { syncContextToMdc(coreSubscriber.currentContext()); try { coreSubscriber.onError(t); } finally { remove(MDC_USER_ID_KEY); } } @Override public void onComplete() { coreSubscriber.onComplete(); } private void syncContextToMdc(ContextView context) { context.getOrEmpty(CONTEXT_USER_KEY) .ifPresent(username -> put(MDC_USER_ID_KEY, username)); } }); } }
步骤3:日志配置添加user_id字段
确保你的日志配置(比如logback.xml)中,日志格式包含%X{user_id},示例:
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %X{user_id} - %msg%n</pattern>
关键说明
- Reactor Context会跟随整个响应式请求流,不管线程切换到reactor还是parallel池,都能获取到用户名。
- Hook会在每个操作执行前自动将Context中的用户名写入MDC,执行后清理,既保证日志能拿到字段,又避免线程复用导致的MDC数据污染。
- WebFilter在请求最开始阶段执行,因此初始服务器请求日志也能带上
user_id,解决了Controller设置MDC的滞后问题。
内容的提问来源于stack exchange,提问作者Dariusz Urbanek
相关产品推荐
相关产品推荐

