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

Spring Webflux如何为同一端点配置Basic Auth与JWT多认证方式

Spring Webflux 同一端点支持JWT与Basic Auth双认证的配置方案

问题描述

我的Spring Webflux应用需要为API提供两种认证方式:用户可选择携带JWT令牌或用户名密码(Basic Auth)访问/api/something/**端点,其余端点仅支持JWT认证。目前配置了两个独立的SecurityWebFilterChain Bean,分别对应Basic Auth和JWT认证,但当访问/api/something/**时,首次认证失败会直接返回401 Unauthorized,不会尝试第二种认证方式。

当前配置代码:

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {

  @Autowired private SecurityContextRepository securityContextRepository;

  @Bean
  SecurityWebFilterChain webHttpSecurity(
          ServerHttpSecurity http, BasicAuthenticationManager authenticationManager) {
    http.securityMatcher(new PathPatternParserServerWebExchangeMatcher("/api/something/**"))
            .authenticationManager(authenticationManager)
            .authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
            .httpBasic()
            .and()
            .csrf()
            .disable();
    return http.build();
  }

  @Bean
  SecurityWebFilterChain springWebFilterChain(
      ServerHttpSecurity http, AuthenticationManager authenticationManager) {
    String[] patterns =
        new String[] {
          "/v2/api-docs",
          "/configuration/ui",
          "/swagger-resources/**",
          "/configuration/**",
          "/swagger-ui/**",
          "/swagger-ui.html",
          "/v3/api-docs/**",
          "/webjars/**",
        };
    return http.cors()
        .disable()
        .exceptionHandling()
        .authenticationEntryPoint(
            (swe, e) ->
                Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED)))
        .accessDeniedHandler(
            (swe, e) ->
                Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN)))
        .and()
        .csrf()
        .disable()
        .authenticationManager(authenticationManager)
        .securityContextRepository(securityContextRepository)
        .authorizeExchange()
        .pathMatchers(patterns)
        .permitAll()
        .pathMatchers(HttpMethod.OPTIONS)
        .permitAll()
        .anyExchange()
        .authenticated()
        .and()
        .build();
  }
}

解决方案

问题根源

多个SecurityWebFilterChain匹配同一端点时,Spring Security会按Bean加载顺序优先匹配第一个FilterChain,若该FilterChain内的认证失败,会直接触发其配置的认证失败逻辑返回401,不会继续执行后续的FilterChain。

配置修改思路

将两种认证方式整合到同一个SecurityWebFilterChain中,通过添加**多个AuthenticationWebFilter**实现依次尝试认证:只要其中一个Filter认证成功,就会设置SecurityContext并允许请求通过;若所有Filter认证都失败,才返回401。

修改后的完整配置

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {

    @Autowired
    private SecurityContextRepository securityContextRepository;

    @Bean
    SecurityWebFilterChain combinedSecurityFilterChain(ServerHttpSecurity http,
                                                      BasicAuthenticationManager basicAuthenticationManager,
                                                      AuthenticationManager jwtAuthenticationManager) {
        // 定义开放路径
        String[] openPatterns = new String[]{
                "/v2/api-docs",
                "/configuration/ui",
                "/swagger-resources/**",
                "/configuration/**",
                "/swagger-ui/**",
                "/swagger-ui.html",
                "/v3/api-docs/**",
                "/webjars/**"
        };

        // 1. 创建Basic Auth认证Filter
        AuthenticationWebFilter basicAuthFilter = new AuthenticationWebFilter(basicAuthenticationManager);
        // 设置Basic Auth的请求转换器(解析Authorization头中的Basic凭证)
        basicAuthFilter.setServerAuthenticationConverter(new BasicAuthenticationConverter());
        // 仅对/api/something/**路径尝试Basic Auth认证
        basicAuthFilter.setRequiresAuthenticationMatcher(
                new PathPatternParserServerWebExchangeMatcher("/api/something/**"));
        // 认证失败时不直接返回,让后续Filter继续尝试
        basicAuthFilter.setAuthenticationFailureHandler((exchange, exception) -> Mono.empty());

        // 2. 创建JWT认证Filter
        AuthenticationWebFilter jwtAuthFilter = new AuthenticationWebFilter(jwtAuthenticationManager);
        // 设置自定义JWT转换器(解析Authorization头中的Bearer令牌)
        jwtAuthFilter.setServerAuthenticationConverter(new ServerAuthenticationConverter() {
            @Override
            public Mono<Authentication> convert(ServerWebExchange exchange) {
                // 替换为你的JWT令牌解析逻辑,生成Authentication对象
                String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
                if (authHeader == null || !authHeader.startsWith("Bearer ")) {
                    return Mono.empty();
                }
                String token = authHeader.substring(7);
                // 假设JwtAuthenticationToken是你的自定义认证令牌类
                return Mono.just(new JwtAuthenticationToken(token));
            }
        });
        // 关联SecurityContextRepository,用于存储认证上下文
        jwtAuthFilter.setSecurityContextRepository(securityContextRepository);
        // 对所有非开放路径尝试JWT认证
        jwtAuthFilter.setRequiresAuthenticationMatcher(
                ServerWebExchangeMatchers.pathMatchers(PathMatchers.any())
                        .and(ServerWebExchangeMatchers.pathMatchers(openPatterns).negate()));

        // 3. 构建统一的SecurityFilterChain
        return http.cors().disable()
                .csrf().disable()
                .exceptionHandling()
                .authenticationEntryPoint((swe, e) ->
                        Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED)))
                .accessDeniedHandler((swe, e) ->
                        Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN)))
                .and()
                // 添加Basic Auth Filter,优先级低于JWT(可根据需求调整顺序)
                .addFilterAfter(basicAuthFilter, SecurityWebFiltersOrder.AUTHENTICATION)
                // 添加JWT Filter
                .addFilterBefore(jwtAuthFilter, SecurityWebFiltersOrder.AUTHENTICATION)
                .authorizeExchange()
                .pathMatchers(openPatterns).permitAll()
                .pathMatchers(HttpMethod.OPTIONS).permitAll()
                // /api/something/**允许通过任一认证方式访问
                .pathMatchers("/api/something/**").authenticated()
                // 其余端点需认证(仅JWT能处理)
                .anyExchange().authenticated()
                .and()
                .build();
    }
}

关键说明

  • 多Filter顺序:通过addFilterBefore/addFilterAfter控制Filter的执行顺序,示例中先执行JWT认证,失败后再尝试Basic Auth,可根据业务需求调整顺序。
  • 认证失败处理:将Basic Auth Filter的AuthenticationFailureHandler设置为返回Mono.empty(),这样认证失败时不会立即返回响应,而是让后续Filter继续处理。
  • 请求匹配器:为每个Filter设置独立的RequiresAuthenticationMatcher,确保Basic Auth仅作用于目标端点,JWT作用于所有需要认证的非开放端点。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 18:15:46