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

Spring WebFlux中JWT与请求体校验的请求传递异常问题

使用场景

校验JWT中的数据与请求体是否匹配。JWT的sub字段内容如下:

{
  "sub": "{\"data\": {\"street\": \"Street 1\", \"landmark\": \"TallestTower\"}}"
}

请求体必须包含完全一致的数据,不匹配则返回禁止访问,匹配则放行至后续处理。

技术栈
  • Springboot 3
  • WebFlux响应式路由函数
  • Jose4j处理JWT
问题现象

调试时发现,执行到ResourceRoutesHandler.create()方法,尝试执行request.bodyToMono().flatMap()时,请求直接以HTTP 200状态结束,但.flatMap()内的后续逻辑并未执行,且工作区无ServerResponse.ok()代码触发该状态。

问题推测

认为是ServerRequest.from(request).build()导致的异常。

诉求

请问是否有办法在校验请求体后,将同一个请求或在同一订阅中传递至后续处理器?或者有没有其他方法可以在请求到达路由处理器前校验请求数据?

相关代码

Routes

@Configuration
@RequiredArgsConstructor
public class ResourceRoutes {

  final JwtHandlerFilterFunction jwtHandlerFilterFunction;

  public RouterFunction<ServerResponse> createResource(ResourceRoutesHandler recourceRoutesHandler) {
    return RouterFunction.route(
            POST("/create").and(accept(MediaType.APPLICATION_JSON)),
            recourceRoutesHandler::create
        )
        .filter(jwtHandlerFilterFunction);
  }
}

HandlerFilterFunction

@Component()
@RequiredArgsConstructor
public class JwtHandlerFilterFunction
    implements HandlerFilterFunction<ServerResponse, ServerResponse> {

  final ObjectMapper objectMapper;

  @Override
  public Mono<ServerResponse> filter(final ServerRequest request,
                                     final HandlerFunction<ServerResponse> next) {

    Headers headers = request.headers();
  
    Optional<String> jwtToken = Optional.ofNullable(headers.firstHeader("Auth-Token"));

    return jwtToken.map(_jwt -> jwtValidateService
                       .validateReactive(_jwt)
                       .flatMap(verifyPayload(request))
                       .flatMap(validMessage -> next.handle(
                        // clone here and pass on to next
                        // this is causing fail to execute request in route handler
                           ServerRequest.from(request)
                                        .build()
                       ))
                       .onErrorResume(throwable -> ServerResponse.status(HttpStatus.FORBIDDEN)
                           .contentType(MediaType.APPLICATION_JSON)
                           .bodyValue("Authorization failed")
                       ) // jwt validation failed
                   )
                   .orElse(next.handle(request)); // to next if header not present
  }

  private Function<String, Mono<String>> verifyPayload(final ServerRequest clientRequest) {
    return subject -> clientRequest
        .bodyToMono(String.class) // as the body already read how to pass on to next handler?
        .flatMap(requestData -> Mono.fromCallable(() -> {
          // for simplicity
          if (!subject.equals(requestData)) {
            throw new RuntimeException("Invalid subject data");
          }
          return "Valid";
        }));
  }

}
解决方案

问题核心在于请求体只能被读取一次:在Filter里调用clientRequest.bodyToMono(String.class)已经消费了请求体的数据流,后续处理器再尝试读取时会拿到空数据,导致逻辑无法执行,最终返回200但无实际处理逻辑。

以下是两种可行的解决思路:


思路1:缓存请求体并传递给后续处理器

在Filter中读取请求体后缓存,再构建包含缓存内容的ServerRequest传递给下一个处理器:

  1. 修改verifyPayload方法,同时返回校验结果和请求体:
private Function<String, Mono<Tuple2<String, String>>> verifyPayload(final ServerRequest clientRequest) {
    return subject -> clientRequest
        .bodyToMono(String.class)
        .flatMap(requestData -> Mono.fromCallable(() -> {
            if (!subject.equals(requestData)) {
                throw new RuntimeException("Invalid subject data");
            }
            // 返回校验结果+请求体
            return Tuples.of("Valid", requestData);
        }));
}
  1. 在filter方法中用缓存的请求体构建新请求:
return jwtToken.map(_jwt -> jwtValidateService
                   .validateReactive(_jwt)
                   .flatMap(verifyPayload(request))
                   .flatMap(tuple -> {
                       // 复制原请求属性,替换body为缓存内容
                       ServerRequest cachedRequest = ServerRequest.from(request)
                           .body(Mono.just(tuple.getT2()))
                           .build();
                       return next.handle(cachedRequest);
                   })
                   .onErrorResume(throwable -> ServerResponse.status(HttpStatus.FORBIDDEN)
                       .contentType(MediaType.APPLICATION_JSON)
                       .bodyValue("Authorization failed")
                   )
               )
               .orElse(next.handle(request));

思路2:用WebFilter提前拦截校验(更推荐)

WebFilter拦截时机早于路由匹配,可通过ServerWebExchange的mutate()方法缓存请求体,避免重复读取问题:

@Component
@RequiredArgsConstructor
public class JwtValidationWebFilter implements WebFilter {

    private final ObjectMapper objectMapper;
    private final JwtValidateService jwtValidateService;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String jwtToken = exchange.getRequest().getHeaders().getFirst("Auth-Token");
        if (jwtToken == null) {
            return chain.filter(exchange);
        }

        // 读取并缓存请求体
        return exchange.getRequest().getBody()
            .collectList()
            .map(dataBuffers -> {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                dataBuffers.forEach(buffer -> {
                    byte[] bytes = new byte[buffer.readableByteCount()];
                    buffer.read(bytes);
                    DataBufferUtils.release(buffer);
                    try {
                        baos.write(bytes);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                });
                return baos.toString(StandardCharsets.UTF_8);
            })
            .flatMap(requestBody -> jwtValidateService.validateReactive(jwtToken)
                .flatMap(subject -> {
                    if (!subject.equals(requestBody)) {
                        exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
                        return exchange.getResponse().writeWith(Mono.just(
                            exchange.getResponse().bufferFactory().wrap("Authorization failed".getBytes())
                        ));
                    }
                    // 构建带缓存请求体的新请求
                    ServerHttpRequest cachedRequest = exchange.getRequest().mutate()
                        .body(Mono.just(requestBody.getBytes(StandardCharsets.UTF_8)))
                        .build();
                    return chain.filter(exchange.mutate().request(cachedRequest).build());
                })
            )
            .onErrorResume(throwable -> {
                exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
                return exchange.getResponse().writeWith(Mono.just(
                    exchange.getResponse().bufferFactory().wrap("Authorization failed".getBytes())
                ));
            });
    }
}

这种方式的优势:

  • 拦截时机更早,避免路由匹配后的无效处理
  • 通过ServerWebExchange.mutate()安全缓存请求体,确保后续处理器正常读取

额外注意事项

  1. 若要忽略序列化格式差异(如空格、引号位置),不要直接比较字符串,可将两者解析为对象后对比:
// 示例:解析为对象后比较
JwtSubDto subDto = objectMapper.readValue(subject, JwtSubDto.class);
RequestDto requestDto = objectMapper.readValue(requestBody, RequestDto.class);
if (!Objects.equals(subDto.getData(), requestDto.getData())) {
    throw new RuntimeException("Invalid subject data");
}
  1. 响应式编程中需确保所有数据流被正确订阅处理,避免静默失败。

内容的提问来源于stack exchange,提问作者Entity Unknown

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 06:50:27