如何移除Spring Authorization Server默认AuthenticationProviders
问题场景
在Spring Boot后端使用Spring Authorization Server实现App的OAuth2认证时,自定义了OAuth2RefreshTokenAuthProvider,加入了device_id校验逻辑:当请求中的device_id与存储的不匹配时,抛出OAuth2AuthenticationException。但框架默认的OAuth2RefreshTokenAuthenticationProvider仍会被加载,导致自定义Provider抛异常后,默认Provider继续执行,触发空指针错误:
java.lang.IllegalArgumentException: value cannot be null at org.springframework.util.Assert.notNull(Assert.java:181) at org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext$AbstractBuilder.put(OAuth2TokenContext.java:219) at org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext$AbstractBuilder.principal(OAuth2TokenContext.java:152) at org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider.authenticate(OAuth2RefreshTokenAuthenticationProvider.java:171) at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182) at org.springframework.security.authentication.ObservationAuthenticationManager.lambda$authenticate$1(ObservationAuthenticationManager.java:54) at io.micrometer.observation.Observation.observe(Observation.java:564) at org.springframework.security.authentication.ObservationAuthenticationManager.authenticate(ObservationAuthenticationManager.java:53)
核心原因
ProviderManager的认证逻辑是:遍历所有支持当前认证类型的Provider,仅在认证成功或抛出AccountStatusException/InternalAuthenticationServiceException时停止遍历;抛出其他AuthenticationException(如OAuth2AuthenticationException)时,会继续执行后续Provider。
此时getProviders()会返回两个支持OAuth2RefreshTokenAuthenticationToken的Provider:自定义的和默认的。自定义Provider先执行并抛异常后,默认Provider仍会运行,由于上下文已被异常影响出现空值,最终触发报错。
解决方案
通过自定义AuthenticationManager并替换Spring Authorization Server默认的实例,仅加入自己的自定义Provider,彻底排除默认Provider。
步骤1:创建自定义AuthenticationManager Bean
@Bean public AuthenticationManager customAuthenticationManager( OAuth2PasswordGrantAuthProvider passwordAuthProvider, OAuth2RefreshTokenAuthProvider refreshTokenAuthProvider ) { return new ProviderManager(Arrays.asList( passwordAuthProvider, refreshTokenAuthProvider )); }
步骤2:在OAuth2配置中使用自定义AuthenticationManager
修改原oAuth2FilterChain方法,将自定义的AuthenticationManager注入并配置到OAuth2AuthorizationServerConfigurer中:
@Configuration public class OAuth2Config { @Bean public SecurityFilterChain oAuth2FilterChain( HttpSecurity http, AuthenticationManager customAuthenticationManager, PasswordGrantAuthenticationConverter passwordGrantAuthenticationConverter, RefreshTokenAuthenticationConverter refreshTokenAuthenticationConverter ) throws Exception { OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer(); configurer // 替换默认AuthenticationManager,仅使用自定义Provider .authenticationManager(customAuthenticationManager) .tokenEndpoint(token -> token .accessTokenRequestConverter( new DelegatingAuthenticationConverter(List.of( passwordGrantAuthenticationConverter, refreshTokenAuthenticationConverter ))) ); http .securityMatcher("/oauth2/**") .with(configurer, (authorizationServer) -> authorizationServer.oidc(Customizer.withDefaults())) .authorizeHttpRequests( auth -> auth .requestMatchers("/oauth2/token").permitAll() .anyRequest().authenticated()) .csrf(csrf -> csrf.ignoringRequestMatchers("/oauth2/**")); return http.build(); } // 以下原有Bean配置保持不变 @Bean public RegisteredClientRepository registeredClientRepository( OAuth2Properties oAuth2Properties ) { TokenSettings tokenSettings = TokenSettings.builder() .accessTokenTimeToLive(Duration.ofMinutes( oAuth2Properties.getFirstScope().getAccessTokenTimeToLiveInMinutes())) .refreshTokenTimeToLive(Duration.ofDays( oAuth2Properties.getFirstScope().getRefreshTokenTimeToLiveInDays())) .reuseRefreshTokens(false) .build(); RegisteredClient firstClient = RegisteredClient.withId( UUID.randomUUID().toString()) .clientId(MyOAuth2.FIRST_CLIENT_ID) .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) .authorizationGrantType(AuthorizationGrantType.PASSWORD) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .scope(MyOAuth2.FIRST_SCOPE) .tokenSettings(tokenSettings) .build(); return new InMemoryRegisteredClientRepository(firstClient); } @Bean public OAuth2AuthorizationService authorizationService( JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository ) { return new JdbcOAuth2AuthorizationService( jdbcTemplate, registeredClientRepository); } }
原理说明
Spring Authorization Server的OAuth2AuthorizationServerConfigurer默认会自动注册一系列默认的AuthenticationProvider。通过显式指定authenticationManager(),我们完全接管了Provider的管理,只加载自己需要的自定义Provider,从而避免默认Provider干扰业务逻辑。
内容的提问来源于stack exchange,提问作者Kira Resari

