如何从ServerWebExchange获取Principal并写入MDC用于日志记录?
在Spring Reactive中从ServerWebExchange获取Principal字符串并存入MDC
在Reactive环境下不能直接阻塞获取Mono<Principal>的值,必须遵循响应式编程规则处理流,下面是可行的实现方案:
核心实现代码
@Component public class RequestTraceFilter implements WebFilter { private static final String REQUEST_ID = "request-id"; private static final String USER_NAME = "user-name"; @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { // 生成随机请求ID并放入MDC String requestId = UUID.randomUUID().toString(); MDC.put(REQUEST_ID, requestId); // 处理Principal,提取用户名存入MDC return exchange.getPrincipal() // 从Principal中提取用户名 .map(Principal::getName) // 未认证请求默认使用anonymous .defaultIfEmpty("anonymous") // 当Mono成功发射用户名时,写入MDC .doOnNext(userName -> MDC.put(USER_NAME, userName)) // 继续执行后续过滤链 .then(chain.filter(exchange)) // 流结束后清理MDC,避免线程复用导致的脏数据 .doFinally(signalType -> { MDC.remove(REQUEST_ID); MDC.remove(USER_NAME); }); } }
关键注意事项
绝对不要用
block()获取Mono值
Reactive环境下调用block()会阻塞线程,破坏非阻塞设计,甚至引发线程池死锁,必须用map/doOnNext这类响应式操作符处理流。处理未认证场景
用defaultIfEmpty("anonymous")处理未登录用户的请求,避免Mono为空时后续逻辑中断。必须清理MDC
WebFlux使用线程池复用线程,doFinally会在流结束(成功/失败/取消)时执行,确保MDC里的请求标识被清除,防止后续请求拿到旧数据。
备选:从ReactiveSecurityContext获取Principal
如果exchange.getPrincipal()无法满足需求,也可以通过安全上下文直接获取认证信息:
return ReactiveSecurityContextHolder.getContext() .map(SecurityContext::getAuthentication) .filter(Objects::nonNull) .map(Authentication::getName) .defaultIfEmpty("anonymous") .doOnNext(userName -> MDC.put(USER_NAME, userName)) .then(chain.filter(exchange)) .doFinally(signalType -> { MDC.remove(REQUEST_ID); MDC.remove(USER_NAME); });
进阶:解决线程切换导致MDC值丢失
WebFlux的操作可能在不同线程间切换,ThreadLocal实现的MDC无法跨线程传递,此时推荐将请求标识存入Reactive Context,再通过日志框架扩展提取:
- 将值写入Reactive Context:
@Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { String requestId = UUID.randomUUID().toString(); return exchange.getPrincipal() .map(Principal::getName) .defaultIfEmpty("anonymous") .flatMap(userName -> chain.filter(exchange) .contextWrite(ctx -> ctx .put("request-id", requestId) .put("user-name", userName))); }
- 自定义Logback转换器从Context提取值(示例):
public class ReactiveContextLogConverter extends ClassicConverter { @Override public String convert(ILoggingEvent event) { ContextView context = ReactiveSecurityContextHolder.getContextView(); return context.getOrDefault("request-id", "unknown"); } }
在logback配置中注册转换器并使用:
<conversionRule conversionWord="requestId" converterClass="com.yourpackage.ReactiveContextLogConverter"/> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - requestId:%requestId, user:%userName - %msg%n</pattern>
内容的提问来源于stack exchange,提问作者Sprivro
相关产品推荐
相关产品推荐

