Spring Cloud Gateway+Security JWT注册登录403错误求助
微服务认证问题排查与解决方案
当前环境与配置
API网关(WebFlux)Security配置
网关基于Spring Cloud Gateway(Reactive栈),配置如下:
@Configuration @EnableWebFluxSecurity public class SecurityConfig { @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity serverHttpSecurity){ serverHttpSecurity .authorizeExchange(exchange -> exchange .pathMatchers( "/api/**", "/eureka/**" ) .permitAll() .anyExchange() .authenticated()) .csrf() .disable() .oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt); return serverHttpSecurity.build(); } }
Identity Service(Servlet栈)Security配置
该服务负责用户注册登录,采用自定义JWT认证方案,配置如下:
@Configuration @EnableWebSecurity @RequiredArgsConstructor @EnableMethodSecurity(securedEnabled = true) public class SecurityConfig { private final JwtFilter jwtAuthFilter; private final AuthenticationProvider authenticationProvider; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{ httpSecurity .cors(withDefaults()) .csrf(AbstractHttpConfigurer::disable) .authorizeRequests(request -> request.regexMatchers( "/api/auth/*", "/v2/api-docs", "/v3/api-docs", "/v3/api-docs/*", "/swagger-resources/", "/swagger-resources/*", "/configuration/ui", "/configuration/security", "/swagger-ui/", "/webjars/*", "/swagger-ui.html" ) .permitAll() .anyRequest() .authenticated() ) .sessionManagement( session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authenticationProvider(authenticationProvider) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); return httpSecurity.build(); } }
遇到的问题
通过Postman向http://localhost:8080/api/auth/register发送携带自定义JWT的POST请求时,返回403 Forbidden。网关日志显示请求已路由到identity-service,但最终返回403:
2024-04-12 20:15:02.924 TRACE [xpense-gateway,,] 35685 --- [ parallel-8] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "/eureka/web" does not match against value "/api/auth/register" 2024-04-12 20:15:02.924 TRACE [xpense-gateway,,] 35685 --- [ parallel-8] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "/api/auth/**" matches against value "/api/auth/register" 2024-04-12 20:15:02.924 DEBUG [xpense-gateway,,] 35685 --- [ parallel-8] o.s.c.g.h.RoutePredicateHandlerMapping : Route matched: identity-service 2024-04-12 20:15:02.924 DEBUG [xpense-gateway,,] 35685 --- [ parallel-8] o.s.c.g.h.RoutePredicateHandlerMapping : Mapping [Exchange: POST http://localhost:8080/api/auth/register] to Route{id='identity-service', uri=lb://identity-service, order=0, predicate=Paths: [/api/auth/**], match trailing slash: true, gatewayFilters=[[[SpringCloudCircuitBreakerResilience4JFilterFactory name = 'resilience', fallback = /identity-fallback], order = 0]], metadata={}} 2024-04-12 20:15:02.924 DEBUG [xpense-gateway,,] 35685 --- [ parallel-8] o.s.c.g.h.RoutePredicateHandlerMapping : [ade0fee6-4] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler@56568494 2024-04-12 20:15:02.924 DEBUG [xpense-gateway,,] 35685 --- [ parallel-8] o.s.c.g.handler.FilteringWebHandler : Sorted gatewayFilterFactories: [[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@7a3a49e5}, order = -2147483648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@305881b8}, order = -2147482648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@6dbb3d7d}, order = -1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@3ea9a091}, order = 0], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.GatewayMetricsFilter@26495639}, order = 0], [[SpringCloudCircuitBreakerResilience4JFilterFactory name = 'resilience', fallback = /identity-fallback], order = 0], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@6c1b82cd}, order = 10000], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter@54687fd0}, order = 10150], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.LoadBalancerServiceInstanceCookieFilter@6eaf030c}, order = 10151], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@16f4a3c0}, order = 2147483646], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@b2da3a5}, order = 2147483647], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@acd3460}, order = 2147483647]] 2024-04-12 20:15:02.924 TRACE [xpense-gateway,ffa6cb10b03275d4,5984f380554abaa0] 35685 --- [ parallel-8] o.s.c.g.filter.RouteToRequestUrlFilter : RouteToRequestUrlFilter start 2024-04-12 20:15:02.924 TRACE [xpense-gateway,ffa6cb10b03275d4,5984f380554abaa0] 35685 --- [ parallel-8] s.c.g.f.ReactiveLoadBalancerClientFilter : ReactiveLoadBalancerClientFilter url before: lb://identity-service/api/auth/register 2024-04-12 20:15:02.925 TRACE [xpense-gateway,ffa6cb10b03275d4,5984f380554abaa0] 35685 --- [ parallel-8] s.c.g.f.ReactiveLoadBalancerClientFilter : LoadBalancerClientFilter url chosen: http://VFIEVOX3.Router:35185/api/auth/register 2024-04-12 20:15:02.945 TRACE [xpense-gateway,,] 35685 --- [r-http-epoll-11] o.s.c.gateway.filter.NettyRoutingFilter : outbound route: dc99dbca, inbound: [ade0fee6-4] 2024-04-12 20:15:02.951 TRACE [xpense-gateway,,] 35685 --- [r-http-epoll-11] o.s.c.g.filter.NettyWriteResponseFilter : NettyWriteResponseFilter start inbound: dc99dbca, outbound: [ade0fee6-4] 2024-04-12 20:15:02.952 TRACE [xpense-gateway,,] 35685 --- [r-http-epoll-11] o.s.c.g.filter.GatewayMetricsFilter : spring.cloud.gateway.requests tags: [tag(httpMethod=POST),tag(httpStatusCode=403),tag(outcome=CLIENT_ERROR),tag(routeId=identity-service),tag(routeUri=lb://identity-service),tag(status=FORBIDDEN)]
核心疑问
SecurityFilterChain与SecurityWebFilterChain的具体区别- 如何在网关的
SecurityWebFilterChain中实现无状态会话管理 - 如何脱离Keycloak,搭建自定义的全链路认证授权机制
解决方案
1. 403错误根源排查
从网关日志可知请求已成功路由到identity-service,403的核心原因是网关的JWT验证不通过:
- 网关配置了
.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt),该配置会让网关使用Keycloak的JWT验证规则(签名算法、issuer、audience等)校验请求中的Token - 而你使用的是自定义生成的JWT,与Keycloak的Token格式/签名规则不匹配,导致网关直接拒绝请求
2. SecurityFilterChain vs SecurityWebFilterChain
| 类型 | 适用场景 | 核心API | 编程模型 |
|---|---|---|---|
| SecurityFilterChain | 传统Servlet栈Spring Boot应用 | HttpSecurity | 同步阻塞 |
| SecurityWebFilterChain | Reactive栈应用(如Gateway) | ServerHttpSecurity | 异步非阻塞 |
两者分别对应Spring MVC和Spring WebFlux技术栈,配置API与底层处理逻辑完全独立。
3. 网关实现无状态会话管理
WebFlux Security默认就是无状态的,Reactive栈本身不依赖Servlet会话。若要强化无状态特性,可添加以下配置:
@Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity serverHttpSecurity) { serverHttpSecurity // ... 其他配置 .securityContextRepository(NoOpServerSecurityContextRepository.getInstance()); // 禁用安全上下文存储,彻底无状态 return serverHttpSecurity.build(); }
无需额外配置会话管理,只要不启用依赖会话的认证方式(如表单登录)即可。
4. 自定义全链路认证改造
网关侧改造(替换Keycloak资源服务器为自定义JWT验证)
移除Keycloak相关配置,实现自定义Reactive JWT认证:
@Configuration @EnableWebFluxSecurity public class SecurityConfig { @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity serverHttpSecurity, ReactiveAuthenticationManager jwtAuthManager) { serverHttpSecurity .authorizeExchange(exchange -> exchange .pathMatchers("/api/auth/**", "/eureka/**") .permitAll() .anyExchange() .authenticated()) .csrf().disable() .authenticationManager(jwtAuthManager) .securityContextRepository(NoOpServerSecurityContextRepository.getInstance()); return serverHttpSecurity.build(); } // 自定义Reactive JWT认证管理器 @Bean public ReactiveAuthenticationManager jwtReactiveAuthenticationManager(JwtDecoder jwtDecoder) { JwtReactiveAuthenticationManager manager = new JwtReactiveAuthenticationManager(jwtDecoder); // 可扩展自定义JWT转换逻辑,将JWT Claims转换为Authentication对象 manager.setJwtAuthenticationConverter(jwt -> { return Mono.just(new UsernamePasswordAuthenticationToken( jwt.getSubject(), null, Collections.emptyList() )); }); return manager; } // 自定义JWT解码器,需与identity-service的Token生成规则一致 @Bean public JwtDecoder jwtDecoder() { // 示例:使用对称密钥验证签名,密钥需与identity-service生成Token时一致 SecretKey secretKey = Keys.hmacShaKeyFor("your-strong-custom-secret-key-1234567890".getBytes(StandardCharsets.UTF_8)); return NimbusJwtDecoder.withSecretKey(secretKey).build(); } }
Identity Service侧优化
- 修正路径匹配规则:将
regexMatchers("/api/auth/*")改为antMatchers("/api/auth/**"),避免正则匹配的局限性 - 确保
JwtFilter正确解析Token,AuthenticationProvider能正确验证用户身份信息
5. 快速验证步骤
- 直接请求identity-service的端口(如
http://localhost:35185/api/auth/register),确认服务本身是否正常 - 检查网关与identity-service的JWT密钥、签名算法是否完全一致
- 开启Spring Security DEBUG日志,查看具体认证失败细节:
logging.level.org.springframework.security=DEBUG
内容的提问来源于stack exchange,提问作者Aniket Angwalkar
相关产品推荐
相关产品推荐

