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
相关产品推荐
相关产品推荐

