Spring Cloud Gateway全局过滤器仅部分服务生效问题求助
我在微服务应用中使用Spring Cloud Gateway,通过HttpOnly Cookie管理令牌安全。实现了一个GlobalFilter,负责从Cookie提取令牌并添加到受保护接口的请求头里。Spring Security配置设置为:认证请求(登录、注册等)允许匿名访问,其他请求必须认证。
登录接口调用正常,过滤器日志也显示被调用;但登录后Angular主页调用users微服务获取用户信息时,一直返回HTTP 401,且日志显示GlobalFilter根本没被触发。试过从WebFilter改成GlobalFilter,调整执行优先级,但问题依旧。
相关代码与配置
过滤器代码
package com.omb.ombgateway.filter; import com.omb.ombgateway.client.RefreshTokenRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Slf4j @RequiredArgsConstructor public class AuthorizationTokenFilter implements GlobalFilter, Ordered { private final WebClient tokenWebClient; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { log.error("FILTER CALLED for {}", exchange.getRequest().getURI()); String token = extractCookie(exchange, "token"); String refreshToken = extractCookie(exchange, "refreshToken"); if (token != null && !token.isEmpty()) { ServerHttpRequest mutatedRequest = exchange.getRequest().mutate() .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) .build(); exchange = exchange.mutate().request(mutatedRequest).build(); } ServerWebExchange finalExchange = exchange; return chain.filter(exchange) .onErrorResume(e -> { if (e instanceof InvalidBearerTokenException && refreshToken != null) { log.info("Invalid token, trying refresh..."); return tokenWebClient.post() .uri("http://localhost:8084/api/1.0/auth/refresh") .bodyValue(new RefreshTokenRequest(refreshToken)) .retrieve() .bodyToMono(String.class) .flatMap(newToken -> { ServerHttpRequest newRequest = finalExchange.getRequest().mutate() .header(HttpHeaders.AUTHORIZATION, "Bearer " + newToken) .build(); ServerWebExchange newExchange = finalExchange.mutate().request(newRequest).build(); newExchange.getResponse().addCookie(ResponseCookie.from("token", newToken) .httpOnly(true) .secure(true) .path("/") .build()); return chain.filter(newExchange); }); } return Mono.error(e); }); } private String extractCookie(ServerWebExchange exchange, String name) { return exchange.getRequest().getCookies().getFirst(name) != null ? exchange.getRequest().getCookies().getFirst(name).getValue() : null; } @Override public int getOrder() { return SecurityWebFiltersOrder.AUTHENTICATION.getOrder() - 1; } }
过滤器配置类
package com.omb.ombagateway.configuration; import com.omb.qabilagateway.filter.AuthorizationTokenFilter; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; @Configuration public class GatewayFilterConfig { @Bean public GlobalFilter authorizationFilter(WebClient tokenWebClient) { return new AuthorizationTokenFilter(tokenWebClient); } }
WebClient配置类(替代Feign解决循环依赖)
package com.omb.qabilagateway.configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; @Configuration public class WebClientConfig { @Bean public WebClient tokenWebClient() { return WebClient.builder().build(); } }
Spring Security配置
package com.omb.qabilagateway.configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.CorsConfigurationSource; import java.util.List; @Configuration public class SecurityConfig { @Bean public SecurityWebFilterChain gatewaySecurity(ServerHttpSecurity http) { http .csrf(csrf -> csrf.disable()) .securityContextRepository(NoOpServerSecurityContextRepository.getInstance()) .authorizeExchange(exchanges -> exchanges .pathMatchers("/api/1.0/auth/**", "/actuator/**", "/error").permitAll() .anyExchange().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())); http.cors(cors -> { CorsConfigurationSource source = request -> { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(List.of("http://localhost:4200")); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(List.of("*")); configuration.setAllowCredentials(true); return configuration; }; cors.configurationSource(source); }); return http.build(); } }
网关日志
日志显示仅/login接口触发了过滤器,/users接口无任何过滤器调用记录:
2025-04-15 14:47:07,036 DEBUG [parallel-6] o.s.c.g.h.RoutePredicateHandlerMapping: Route matched: auth-service 2025-04-15 14:47:07,036 DEBUG [parallel-6] o.s.c.g.h.RoutePredicateHandlerMapping: Mapping [Exchange: POST http://localhost:8084/api/1.0/auth/login] to Route{id='auth-service', uri=lb://auth-service, order=0, predicate=Paths: [/api/1.0/auth/**], match trailing slash: true, gatewayFilters=[], metadata={}} 2025-04-15 14:47:07,036 DEBUG [parallel-6] o.s.w.r.h.AbstractHandlerMapping: [e522340a-6] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler@3ca17943 2025-04-15 14:47:07,038 DEBUG [parallel-6] o.s.c.g.h.FilteringWebHandler: Sorted gatewayFilterFactories: [[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@27df5806}, order = -2147483648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@3271ec2a}, order = -2147482648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@365cef67}, order = -1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@52d7ab79}, order = 0], [GatewayFilterAdapter{delegate=com.omb.ombgateway.filter.AuthorizationTokenFilter@2049e582}, order = 799], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@69bc9584}, order = 10000], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter@464aeb09}, order = 10150], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.LoadBalancerServiceInstanceCookieFilter@2c99c8d}, order = 10151], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@f95d64d}, order = 2147483646], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@59d6642a}, order = 2147483647], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@3214bad}, order = 2147483647]] 2025-04-15 14:47:07,038 ERROR [parallel-6] c.o.q.f.AuthorizationTokenFilter: FILTER CALLED for http://localhost:8084/api/1.0/auth/login
网关路由配置
spring: application: name: api-gateway cloud: gateway: routes: - id: auth-service uri: lb://auth-service predicates: - Path=/api/1.0/auth/** - id: account-service uri: lb://account-service predicates: - Path=/api/1.0/users/** globalcors: corsConfigurations: '[/**]': allowedOrigins: - "http://localhost:4200" allowedMethods: - GET - POST - PUT - DELETE - OPTIONS allowedHeaders: - "*" allowCredentials: true security: oauth2: resourceserver: jwt: issuer-uri: http://localhost:8080/realms/My_Realm server: port: 8084 error: include-stacktrace: always eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ instance: prefer-ip-address: true
排查与解决方案
核心问题分析
从日志可以看到,/users请求的过滤器链根本没走到自定义的AuthorizationTokenFilter,原因是Spring Security的认证过滤器在自定义GlobalFilter之前执行,导致请求先被拦截返回401,自定义过滤器没机会处理。
虽然设置了SecurityWebFiltersOrder.AUTHENTICATION.getOrder() -1,但Spring Cloud Gateway的GlobalFilter顺序和Spring Security WebFilter的顺序是两个不同的体系,直接混用会导致顺序错误。
解决方案
1. 调整过滤器类型与执行顺序
把自定义过滤器从GlobalFilter改为Spring Security的WebFilter,并指定在认证过滤器之前执行:
@Slf4j @RequiredArgsConstructor @Component public class AuthorizationTokenFilter implements WebFilter, Ordered { private final WebClient tokenWebClient; @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { log.error("FILTER CALLED for {}", exchange.getRequest().getURI()); String token = extractCookie(exchange, "token"); String refreshToken = extractCookie(exchange, "refreshToken"); if (token != null && !token.isEmpty()) { ServerHttpRequest mutatedRequest = exchange.getRequest().mutate() .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) .build(); exchange = exchange.mutate().request(mutatedRequest).build(); } ServerWebExchange finalExchange = exchange; return chain.filter(exchange) .onErrorResume(e -> { if (e instanceof InvalidBearerTokenException && refreshToken != null) { log.info("Invalid token, trying refresh..."); return tokenWebClient.post() .uri("http://localhost:8084/api/1.0/auth/refresh") .bodyValue(new RefreshTokenRequest(refreshToken)) .retrieve() .bodyToMono(String.class) .flatMap(newToken -> { ServerHttpRequest newRequest = finalExchange.getRequest().mutate() .header(HttpHeaders.AUTHORIZATION, "Bearer " + newToken) .build(); ServerWebExchange newExchange = finalExchange.mutate().request(newRequest).build(); newExchange.getResponse().addCookie(ResponseCookie.from("token", newToken) .httpOnly(true) .secure(true) .path("/") .build()); return chain.filter(newExchange); }); } return Mono.error(e); }); } private String extractCookie(ServerWebExchange exchange, String name) { return exchange.getRequest().getCookies().getFirst(name) != null ? exchange.getRequest().getCookies().getFirst(name).getValue() : null; } @Override public int getOrder() { return SecurityWebFiltersOrder.AUTHENTICATION.getOrder() - 1; } }
同时移除GatewayFilterConfig中的GlobalFilter配置,因为现在过滤器是Spring Security的WebFilter,通过@Component注册即可。
2. 验证Cookie传递正确性
确保Angular请求时携带了withCredentials: true,否则浏览器不会发送HttpOnly Cookie:
// Angular HTTP请求示例 this.http.get('/api/1.0/users/me', { withCredentials: true }).subscribe(...);
3. 检查Cookie属性配置
登录接口返回Cookie时,确保:
path=/:保证所有路径都能携带该CookieSameSite=None(如果前后端跨域):避免浏览器因SameSite策略阻止Cookie传递secure=true仅在HTTPS环境下使用,本地开发可暂时改为false测试
4. 调试过滤器链顺序
添加日志确认过滤器执行顺序,确保自定义过滤器在JWT认证过滤器之前运行:
// 在SecurityConfig中添加日志 http.addFilterBefore(authorizationTokenFilter, SecurityWebFiltersOrder.AUTHENTICATION);
总结
问题根源是Spring Cloud Gateway的GlobalFilter和Spring Security的WebFilter执行顺序体系不兼容,导致自定义过滤器在认证拦截之后才执行。将过滤器改为Spring Security的WebFilter并指定正确顺序,同时确保前端请求携带凭证,即可解决401问题。
内容的提问来源于stack exchange,提问作者ousmane MBINTE

