响应式编程场景下Spring Webflux WebFilter执行逻辑异常问题求助
根因分析
为什么后续逻辑未执行
你目前只给第一个accessPointService.getAccessPointAuthorizationRequirement返回的Mono加了log()打印,后续的流执行过程没有日志输出,实际上你的流在第一步GET请求拿完CSRF之后的环节已经报错终止了,只是你没有捕获打印异常,所以误以为逻辑没执行。最核心的错误点如下:
- 登录流程完全顺序错误
你用zipWith让获取CSRF的GET请求、获取SESSION的POST请求并行执行,但Spring Security开启CSRF校验的前提下,发POST请求到/login登录接口必须携带有效的CSRF令牌,你并行发送的POST登录请求根本没有带CSRF信息,直接被403拦截,拿不到SESSION,流直接抛出异常终止。 - 没有做是否需要认证的分支判断
你拿到isRequired参数后,不管返回值是true还是false,都会继续执行后面的认证流程,完全不符合业务需求,也会造成不必要的性能浪费。 - 异常处理逻辑缺失
你的onErrorResume直接把异常抛出,没有任何日志打印,完全无法排查中间出现的错误,比如GET请求拿不到XSRF-TOKEN的空指针、POST登录被403拦截的错误都会被直接吞掉,你看不到任何报错信息。 - 凭证传递逻辑错误
你拿到SESSION后放到了Authorization请求头里,如果你的authcheck服务是从Cookie中读取SESSION校验,就会直接校验失败。另外POST登录请求没有携带CSRF信息,也是登录失败的直接原因。
为什么不需要显式订阅
Spring Webflux 本身是响应式运行时,你在WebFilter中返回的Mono<Void>会由Webflux底层的请求处理链路自动订阅,不需要你手动做订阅操作,你只需要保证所有业务逻辑都正确拼接到这个返回的流中即可。
修正后代码示例
首先把WebClient做成单例注入,不要每次请求都新建实例(WebClient是线程安全的,重复创建会造成不必要的资源浪费):
@Configuration public class WebClientConfig { @Bean public WebClient webClient() { return WebClient.builder() .baseUrl("http://localhost:8080") .build(); } }
然后修正Filter的逻辑:
@Component public class AuthenticationFilter implements WebFilter { // 注入单例WebClient private final WebClient webClient; private final AccessPointService accessPointService; // 构造器注入 public AuthenticationFilter(WebClient webClient, AccessPointService accessPointService) { this.webClient = webClient; this.accessPointService = accessPointService; } @Override public Mono<Void> filter(final ServerWebExchange serverWebExchange, final WebFilterChain webFilterChain) { String method = serverWebExchange.getRequest().getMethod().toString().toUpperCase(); String path = serverWebExchange.getRequest().getPath().toString(); return accessPointService.getAccessPointAuthorizationRequirement(method, path) .log("access-point-check") // 先分支判断是否需要认证 .flatMap(isRequired -> { if (!isRequired) { // 不需要认证直接走后续过滤链 return webFilterChain.filter(serverWebExchange); } // 需要认证,先拿CSRF令牌 return getCsrfToken() // 拿到CSRF后再发登录请求拿SESSION .flatMap(this::getSessionId) // 构造authcheck请求并发请求校验 .flatMap(tokenPair -> checkAuth(tokenPair.csrfToken, tokenPair.sessionId)) // 校验通过走后续过滤链 .flatMap(authPass -> { if (authPass) { return webFilterChain.filter(serverWebExchange); } // 校验不通过返回401 serverWebExchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return serverWebExchange.getResponse().setComplete(); }); }) // 统一异常处理,打印日志再返回错误 .onErrorResume(e -> { e.printStackTrace(); serverWebExchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); return serverWebExchange.getResponse().setComplete(); }); } // 拿CSRF令牌 private Mono<String> getCsrfToken() { return webClient.get() .uri("/login") .exchangeToMono(response -> { // 先判断响应状态正常再拿cookie if (response.statusCode().is2xxSuccessful()) { ResponseCookie csrfCookie = response.cookies().getFirst("XSRF-TOKEN"); if (csrfCookie != null) { return Mono.just(csrfCookie.getValue()); } } return Mono.error(new RuntimeException("获取CSRF令牌失败")); }) .log("get-csrf"); } // 拿SESSION ID private Mono<TokenPair> getSessionId(String csrfToken) { return webClient.post() .uri("/login") .contentType(MediaType.APPLICATION_FORM_URLENCODED) // 带上CSRF信息 .header("X-XSRF-TOKEN", csrfToken) .cookie("XSRF-TOKEN", csrfToken) .body(BodyInserters.fromFormData("username", "username") .with("password", "password")) .exchangeToMono(response -> { if (response.statusCode().is2xxSuccessful()) { ResponseCookie sessionCookie = response.cookies().getFirst("SESSION"); if (sessionCookie != null) { return Mono.just(new TokenPair(csrfToken, sessionCookie.getValue())); } } return Mono.error(new RuntimeException("登录获取SESSION失败")); }) .log("get-session"); } // 调用authcheck校验 private Mono<Boolean> checkAuth(String csrfToken, String sessionId) { return webClient.post() .uri("/authcheck") .contentType(MediaType.APPLICATION_JSON) .header("X-XSRF-TOKEN", csrfToken) .cookie("XSRF-TOKEN", csrfToken) // 注意:如果authcheck服务从Cookie读SESSION就用cookie,从Header读就用header,根据实际情况调整 .cookie("SESSION", sessionId) //.header("Authorization", sessionId) .exchangeToMono(response -> { if (response.statusCode().is2xxSuccessful()) { return response.bodyToMono(String.class) // 这里替换成你实际的校验逻辑 .map(body -> !body.isBlank()); } return Mono.just(false); }) .log("check-auth"); } // 临时存储token的DTO private static class TokenPair { private final String csrfToken; private final String sessionId; public TokenPair(String csrfToken, String sessionId) { this.csrfToken = csrfToken; this.sessionId = sessionId; } } }
优化建议
- 缓存凭证:不要每次请求都调用两次
/login接口拿凭证,你可以把CSRF和SESSION缓存起来,设置和凭证有效期一致的过期时间,过期后再重新获取,能大幅提升性能。 - 避免远程调用:如果authcheck服务和当前服务是同一个应用,直接调用对应业务方法即可,不需要用WebClient发网络请求,省去网络开销。
- 细化异常处理:可以根据不同的异常类型返回不同的错误码和提示信息,方便前端处理。
- 调整日志级别:生产环境可以把debug级别的日志关掉,只保留error和info级别的日志,避免日志过多。
内容的提问来源于stack exchange,提问作者Mauricio
相关产品推荐
相关产品推荐

