升级springboot3后,GlobalFilter中exchange.getRequest().mutate().header(...)报错排查处理。

升级后 GlobalFilter 修改 header 报 UnsupportedOperationException

原代码如下: 使用 exchange.mutate()

ServerHttpRequest.Builder builder = exchange.getRequest().mutate().header(DumboConstants.TRACE_KEY, traceContext.getTraceId()).header(DumboConstants.SPAN_KEY, String.valueOf(traceContext.getSpanId()));

背景:

  • 在Springboot 2 系列中,通过mutate() 之后是可以产生一个新的可变的 ServerHttpRequest 对象,可以对请求头等一些相关属性进行调整修改。也是spring 官方推荐的方式
  • 在升级Springboot3x 之后,通过exchange.mutate()之后修改header 属性一直抛 UnsupportedOperationException。 不能去修改属性。

debug 排查后问题初步分析

在 Spring Boot 2.x 中,exchange.getRequest().mutate().header(…) 是允许的,但在 Spring Boot 3(Spring 6) 之后,这种方式会导致 UnsupportedOperationException。
在 Spring Boot 3 中,exchange.getRequest() 返回 StrictFirewallServerWebExchange,而在 Spring Boot 2 中,它返回的是 Netty 相关的 ServerHttpRequest, Spring Boot 3 引入了更严格的安全防护机制(StrictFirewallServerWebExchange),导致 mutate() 方法无法像以前一样修改请求头。

问题分析

  1. Spring Boot 3 采用了 StrictFirewallServerWebExchange
    • StrictFirewallServerWebExchangeSpring WebFlux 6
@Slf4j @Component @ConditionalOnProperty(value = "security.xss.enabled", havingValue = "true") public class XssFilter implements GlobalFilter, Ordered { @Autowired private XssProperties xss; private final PathPatternParser pathPatternParser = new PathPatternParser(); // 路径模式缓存 private final Cache<String, PathPattern> patternCache = Caffeine.newBuilder() .maximumSize(200) .expireAfterAccess(1, TimeUnit.HOURS) .build(); // 路径匹配结果缓存 private final Cache<String, Boolean> pathMatchCache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(5, TimeUnit.MINUTES) .build(); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); if (!xss.getEnabled()) { return chain.filter(exchange); } // GET DELETE 不过滤 HttpMethod method = request.getMethod(); if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE) { return chain.filter(exchange); } // 非json类型,不过滤 if (!isJsonRequest(exchange)) { return chain.filter(exchange); } String rawPath = request.getURI().getRawPath(); String normalizedPath = normalizePath(rawPath); if (isExcludedPath(normalizedPath)) { log.debug("Excluded path: {}", normalizedPath); return chain.filter(exchange); } ServerHttpRequestDecorator decoratedRequest = processRequest(exchange); return chain.filter(exchange.mutate().request(decoratedRequest).build()) .doOnSuccess(v -> addSecurityHeaders(exchange)); } private void addSecurityHeaders(ServerWebExchange exchange) { ServerHttpResponse response = exchange.getResponse(); if (response.isCommitted()) { return; } xss.getResponseHeaders().forEach((key, value) -> response.getHeaders().addIfAbsent(key, value) ); } private String normalizePath(String path) { try { String decoded = URLDecoder.decode(path, "UTF-8"); return StringUtils.cleanPath(decoded); } catch (UnsupportedEncodingException e) { throw new IllegalStateException("UTF-8 decode error", e); } } private boolean isExcludedPath(String path) { Boolean cached = pathMatchCache.getIfPresent(path); if (cached != null) { return cached; } boolean isExcluded = xss.getExcludePatterns().stream() .anyMatch(pattern -> { PathPattern compiled = patternCache.get(pattern, pathPatternParser::parse); boolean match = compiled != null && compiled.matches(PathContainer.parsePath(path)); log.debug("匹配检查 - 模式: {} 路径: {} 结果: {}", pattern, path, match); return match; // return compiled != null && compiled.matches(PathContainer.parsePath(path)); }); pathMatchCache.put(path, isExcluded); return isExcluded; } private ServerHttpRequestDecorator processRequest(ServerWebExchange exchange) { return new ServerHttpRequestDecorator(exchange.getRequest()) { @Override public Flux<DataBuffer> getBody() { return super.getBody() .publishOn(Schedulers.boundedElastic()) // 使用独立线程池 .map(dataBuffer -> { String content = dataBuffer.toString(StandardCharsets.UTF_8); String cleaned = EscapeUtil.clean(content); return DefaultDataBufferFactory.sharedInstance.wrap(cleaned.getBytes()); }); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.putAll(super.getHeaders()); headers.remove(HttpHeaders.CONTENT_LENGTH); return headers; } }; } @Override public int getOrder() { return -100; } /** * 是否是Json请求 * * @param exchange HTTP请求 */ public boolean isJsonRequest(ServerWebExchange exchange) { String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE); } }@Configuration @Data @ConfigurationProperties(prefix = "security.xss") public class XssProperties { /** * Xss开关 */ private Boolean enabled; /** * Ant风格排除路径列表 */ private List<String> excludePatterns = new ArrayList<>(); /** * 响应头配置 */ private Map<String, String> responseHeaders = new LinkedHashMap<>(); }做xss防护时,使用security: xss: enabled: true exclude-patterns: - "/file-server/**" - "/system-server/*/dataRule/save" - "/strategy-server/strategy/**/edit" response-headers: X-XSS-Protection: "1; mode=block" Content-Security-Policy: "default-src &#39;self&#39;" X-Content-Type-Options: "nosniff"这个配置无法过滤/strategy-server/strategy/edit为什么,则呢么修改
05-16
package com.corilead.utlis; import com.alibaba.fastjson.JSON; import com.casic.cplm.audit.dto.AuditTrailDto; import java.time.Instant; import org.springframework.web.reactive.function.client.*; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; public class ParamFilter implements ExchangeFilterFunction { public static final String CUSTOM_PARAM_KEY = "customParam"; @Override public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) { AuditTrailDto operLog = new AuditTrailDto(); String requestBody = (String) request.attributes().getOrDefault(CUSTOM_PARAM_KEY, ""); setupBaseOperationInfo(operLog, request, requestBody); return next.exchange(request) .flatMap(response -> response.bodyToMono(String.class) .defaultIfEmpty("[empty-body]") .doOnNext(body -> Mono.fromRunnable(() -> { operLog.setDescription(JSON.toJSONString(body)); operLog.setStatus(response.statusCode().value()); System.out.println("OperLog: " + operLog); }).subscribeOn(Schedulers.boundedElastic()).subscribe() ) .then(Mono.just(response)) ) .onErrorResume(e -> handleError(e, operLog)); } private void setupBaseOperationInfo( AuditTrailDto operLog, ClientRequest request, String requestBody) { operLog.setEventName("系统内部调用第三方"); operLog.setExtraParams(requestBody); operLog.setResourceType(request.method().name()); operLog.setEventTime(Instant.now()); } private Mono<ClientResponse> handleError(Throwable e, AuditTrailDto operLog) { operLog.setStatus(500); operLog.setDescription(e.getMessage()); System.out.println("Error operLog: " + operLog); return Mono.error(e); } } 修改代码 拦截后接口无响应
最新发布
06-25
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值