如何在非Web Spring Boot批处理应用中配置ClientRegistrationRepository
问题背景
使用Spring Boot 2.7.1 + spring-boot-starter-batch开发批处理任务,需要两个带不同认证机制的WebClient调用外部API,配置依赖spring.security.oauth2.client系列配置项。
原本引入spring-boot-starter-web后,Spring能自动配置WebClient(依赖ClientRegistrationRepository Bean),但批处理会默认监听8080端口,无法并行启动多个实例,因此需要禁用Web服务器。
禁用Web服务器后(通过配置、代码或移除spring-boot-starter-web依赖),批处理启动失败,报错:
a bean of type 'org.springframework.security.oauth2.client.registration.ClientRegistrationRepository' that could not be found
核心原因是OAuth2ClientAutoConfiguration带有条件注解,要求应用类型为SERVLET,而禁用Web服务器后应用类型变为NONE,导致自动配置不生效:
@AutoConfiguration(before = SecurityAutoConfiguration.class) @ConditionalOnClass({ EnableWebSecurity.class, ClientRegistration.class }) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @Import({ OAuth2ClientRegistrationRepositoryConfiguration.class, OAuth2WebSecurityConfiguration.class }) public class OAuth2ClientAutoConfiguration { }
尝试通过@ImportAutoConfiguration(OAuth2ClientAutoConfiguration.class)强制加载无效,且其导入的两个配置类为非公开,无法直接导入。
解决方法
方案1:手动配置ClientRegistrationRepository及关联Bean
直接读取spring.security.oauth2.client配置,手动创建所需Bean,绕开自动配置的条件限制:
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.List; @Configuration public class OAuth2ClientConfig { @Bean @ConfigurationProperties("spring.security.oauth2.client") public ClientRegistrationProperties clientRegistrationProperties() { return new ClientRegistrationProperties(); } @Bean public ClientRegistrationRepository clientRegistrationRepository(ClientRegistrationProperties properties) { List<ClientRegistration> registrations = properties.getRegistration().values().stream() .map(this::convertToClientRegistration) .toList(); return new InMemoryClientRegistrationRepository(registrations); } private ClientRegistration convertToClientRegistration(ClientRegistrationProperties.Registration registration) { return ClientRegistration.withRegistrationId(registration.getRegistrationId()) .clientId(registration.getClientId()) .clientSecret(registration.getClientSecret()) .clientAuthenticationMethod(ClientAuthenticationMethod.valueOf(registration.getClientAuthenticationMethod())) .authorizationGrantType(AuthorizationGrantType.valueOf(registration.getAuthorizationGrantType())) .redirectUri(registration.getRedirectUri()) .scope(registration.getScope().toArray(new String[0])) .authorizationUri(registration.getAuthorizationUri()) .tokenUri(registration.getTokenUri()) .userInfoUri(registration.getUserInfoUri()) .userNameAttributeName(registration.getUserNameAttributeName()) .jwkSetUri(registration.getJwkSetUri()) .clientName(registration.getClientName()) .build(); } }
注:ClientRegistrationProperties是Spring Boot提供的配置绑定类,用于读取spring.security.oauth2.client下的所有配置项。
方案2:强制设置应用类型为SERVLET并禁用Web服务器
通过配置让Spring Boot认为应用是SERVLET类型,同时禁用Web服务器端口,满足OAuth2ClientAutoConfiguration的条件:
在application.yml中添加:
spring: main: web-application-type: servlet server: port: -1 # 禁用Web服务器端口
或者在启动类中硬编码设置:
@SpringBootApplication public class BatchApplication { public static void main(String[] args) { new SpringApplicationBuilder(BatchApplication.class) .web(WebApplicationType.SERVLET) .run(args); } }
这种方式会触发OAuth2ClientAutoConfiguration的自动配置逻辑,同时不会启动Web服务器监听端口。
方案3:自定义WebClient并直接集成OAuth2认证
如果仅需要WebClient具备OAuth2认证能力,可直接为每个WebClient配置认证逻辑,无需依赖自动配置的ClientRegistrationRepository:
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class WebClientConfig { // 对应第一个API的WebClient @Bean("api1WebClient") public WebClient api1WebClient(OAuth2AuthorizedClientManager authorizedClientManager) { ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Filter = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); oauth2Filter.setDefaultClientRegistrationId("api1"); // 对应配置中的registrationId return WebClient.builder() .filter(oauth2Filter) .baseUrl("https://api1.example.com") .build(); } // 对应第二个API的WebClient @Bean("api2WebClient") public WebClient api2WebClient(OAuth2AuthorizedClientManager authorizedClientManager) { ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Filter = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); oauth2Filter.setDefaultClientRegistrationId("api2"); // 对应配置中的registrationId return WebClient.builder() .filter(oauth2Filter) .baseUrl("https://api2.example.com") .build(); } @Bean public OAuth2AuthorizedClientManager authorizedClientManager( ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) { OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() .clientCredentials() // 根据实际授权类型调整,如authorization_code .build(); DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository); authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); return authorizedClientManager; } }
配合application.yml配置:
spring: security: oauth2: client: registration: api1: client-id: your-api1-client-id client-secret: your-api1-client-secret authorization-grant-type: client_credentials scope: read,write api2: client-id: your-api2-client-id client-secret: your-api2-client-secret authorization-grant-type: client_credentials scope: read provider: api1: token-uri: https://api1.example.com/oauth/token api2: token-uri: https://api2.example.com/oauth/token
内容的提问来源于stack exchange,提问作者Vincent F

