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

响应式编程场景下Spring Webflux WebFilter执行逻辑异常问题求助

根因分析

为什么后续逻辑未执行

你目前只给第一个accessPointService.getAccessPointAuthorizationRequirement返回的Mono加了log()打印,后续的流执行过程没有日志输出,实际上你的流在第一步GET请求拿完CSRF之后的环节已经报错终止了,只是你没有捕获打印异常,所以误以为逻辑没执行。最核心的错误点如下:

  1. 登录流程完全顺序错误
    你用zipWith让获取CSRF的GET请求、获取SESSION的POST请求并行执行,但Spring Security开启CSRF校验的前提下,发POST请求到/login登录接口必须携带有效的CSRF令牌,你并行发送的POST登录请求根本没有带CSRF信息,直接被403拦截,拿不到SESSION,流直接抛出异常终止。
  2. 没有做是否需要认证的分支判断
    你拿到isRequired参数后,不管返回值是true还是false,都会继续执行后面的认证流程,完全不符合业务需求,也会造成不必要的性能浪费。
  3. 异常处理逻辑缺失
    你的onErrorResume直接把异常抛出,没有任何日志打印,完全无法排查中间出现的错误,比如GET请求拿不到XSRF-TOKEN的空指针、POST登录被403拦截的错误都会被直接吞掉,你看不到任何报错信息。
  4. 凭证传递逻辑错误
    你拿到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;
        }
    }
}
优化建议
  1. 缓存凭证:不要每次请求都调用两次/login接口拿凭证,你可以把CSRF和SESSION缓存起来,设置和凭证有效期一致的过期时间,过期后再重新获取,能大幅提升性能。
  2. 避免远程调用:如果authcheck服务和当前服务是同一个应用,直接调用对应业务方法即可,不需要用WebClient发网络请求,省去网络开销。
  3. 细化异常处理:可以根据不同的异常类型返回不同的错误码和提示信息,方便前端处理。
  4. 调整日志级别:生产环境可以把debug级别的日志关掉,只保留error和info级别的日志,避免日志过多。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.28 20:24:03