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传递给下一个处理器:
- 修改
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); })); }
- 在
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()安全缓存请求体,确保后续处理器正常读取
额外注意事项
- 若要忽略序列化格式差异(如空格、引号位置),不要直接比较字符串,可将两者解析为对象后对比:
// 示例:解析为对象后比较 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"); }
- 响应式编程中需确保所有数据流被正确订阅处理,避免静默失败。
内容的提问来源于stack exchange,提问作者Entity Unknown
相关产品推荐
相关产品推荐

