Spring Cloud Gateway下微服务OAuth2互调NPE问题求助
多个微服务部署在Spring Cloud Gateway之后,通过Spring Authorization Server的OAuth2协议实现资源保护,整体运行正常。但部分微服务需要互相调用对方的受保护资源,已为每个微服务在授权服务器注册独立客户端(拥有专属client id和client secret),并在各服务中配置了对应OAuth2客户端,使用WebClient实现跨服务数据获取逻辑。
当从resource-client1通过WebClient调用resource-client2时,出现NullPointerException,错误提示Spring Authorization Server未返回access token,具体错误栈如下:
java.lang.NullPointerException: Cannot invoke "org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse.getAccessToken()" because "tokenResponse" is null at org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider.authorize(ClientCredentialsOAuth2AuthorizedClientProvider.java:87) ~[spring-security-oauth2-client-6.0.1.jar:6.0.1] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): *__checkpoint ⇢ Request to GET http://localhost:8082/hello2 [DefaultWebClient] Original Stack Trace: at org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider.authorize(ClientCredentialsOAuth2AuthorizedClientProvider.java:87) ~[spring-security-oauth2-client-6.0.1.jar:6.0.1] at org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider.authorize(DelegatingOAuth2AuthorizedClientProvider.java:71) ~[spring-security-oauth2-client-6.0.1.jar:6.0.1] at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.authorize(DefaultOAuth2AuthorizedClientManager.java:176) ~[spring-security-oauth2-client-6.0.1.jar:6.0.1] at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.lambda$authorizeClient$22(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:485) ~[spring-security-oauth2-client-6.0.1.jar:6.0.1] at reactor.core.publisher.MonoSupplier.call(MonoSupplier.java:67) ~[reactor-core-3.5.1.jar:3.5.1]
相关配置代码如下:
resource-client1的SecurityConfig
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); return http.build(); } @Bean public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); oauth2Client.setDefaultClientRegistrationId("server"); return WebClient.builder() .apply(oauth2Client.oauth2Configuration()) .build(); } @Bean public OAuth2AuthorizedClientManager authorizedClientManager( ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) { OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() .clientCredentials() .build(); DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( clientRegistrationRepository, authorizedClientRepository); authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); return authorizedClientManager; } }
resource-client1的application.yml
spring: security: oauth2: client: registration: server: scope: ROLE_RESOURCE_CLIENT1 client-id: resource-client1 client-secret: '{noop}secret1' client-authentication-method: basic authorization-grant-type: client_credentials client-name: resource-client1-client-credentials provider: server: token-uri: http://localhost:9001/oauth2/token resourceserver: jwt: issuer-uri: http://localhost:9001
授权服务器的客户端配置
private final static RegisteredClient resource1ServiceClient = RegisteredClient.withId("2") .clientId("resource-client1") .clientSecret("{noop}secret1") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .build(); private final static RegisteredClient resource2ServiceClient = RegisteredClient.withId("3") .clientId("resource-client2") .clientSecret("{noop}secret2") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .build();
1. 修复客户端Scope配置缺失问题
授权服务器注册客户端时未配置任何scope,但resource-client1的配置中指定了scope: ROLE_RESOURCE_CLIENT1。当客户端请求token时携带授权服务器不存在的scope,会导致服务器拒绝返回token,最终出现tokenResponse为null的NPE。
修改授权服务器的客户端注册代码,添加对应scope:
private final static RegisteredClient resource1ServiceClient = RegisteredClient.withId("2") .clientId("resource-client1") .clientSecret("{noop}secret1") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .scope("ROLE_RESOURCE_CLIENT1") // 添加对应scope .build(); private final static RegisteredClient resource2ServiceClient = RegisteredClient.withId("3") .clientId("resource-client2") .clientSecret("{noop}secret2") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .scope("ROLE_RESOURCE_CLIENT2") // 根据resource-client2的配置添加对应scope .build();
2. 验证Token端点可达性
用curl直接测试resource-client1是否能正常获取token:
curl -u resource-client1:secret1 http://localhost:9001/oauth2/token -d "grant_type=client_credentials&scope=ROLE_RESOURCE_CLIENT1"
- 若返回包含
access_token的JSON响应,说明端点正常; - 若返回401,检查client-id/secret是否与授权服务器配置一致;
- 若返回400,检查请求参数是否正确,特别是scope是否匹配。
3. 确认WebClient客户端标识匹配
确保oauth2Client.setDefaultClientRegistrationId("server")中的server,与application.yml里spring.security.oauth2.client.registration下的客户端key完全一致,当前配置已匹配,多客户端场景需注意对应关系。
4. 可选:配置JWT权限注入(针对资源服务器授权)
如果resource-client2需要验证JWT中的权限,需在授权服务器添加TokenCustomizer,将客户端scope注入JWT:
@Bean public OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer() { return context -> { if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(context.getAuthorizationGrantType())) { Set<String> scopes = context.getRegisteredClient().getScopes(); context.getClaims().claim("roles", scopes); } }; }
内容的提问来源于stack exchange,提问作者Rajeev

