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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 23:20:36