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

Spring Cloud Gateway全局过滤器仅部分服务生效问题求助

Spring Cloud Gateway GlobalFilter未触发导致401问题排查

我在微服务应用中使用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=/:保证所有路径都能携带该Cookie
  • SameSite=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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 09:39:53