Spring Boot 2 OAuth2 JWT授权服务刷新令牌配置异常排查
首先,咱们先理清核心问题所在:刷新令牌的处理流程并没有走你配置的AuthenticationManager,而是由Spring Security OAuth2的TokenServices负责,这就是为什么调试时看到了另一个ProviderManager,而且密码登录正常但刷新令牌报错。
为什么会出现这个错误?
当你调用刷新令牌接口时,默认会使用DefaultTokenServices的refreshAccessToken方法。这个方法在加载用户信息时,如果没有明确配置UserDetailsService,会尝试使用PreAuthenticatedAuthenticationProvider,但这个Provider并没有关联你自定义的UserDetailsService,所以抛出了UserDetailsService is required的异常。
另外,你想要实现的「刷新令牌时校验数据库中存储的令牌ID」需求,也不能通过配置AuthenticationProvider来完成——因为刷新令牌的验证逻辑不属于AuthenticationManager的认证流程,而是属于令牌生命周期管理的范畴。
解决方案步骤
1. 自定义TokenServices,加入令牌ID校验逻辑
我们需要继承DefaultTokenServices,重写refreshAccessToken方法,在执行默认刷新逻辑之前,先校验数据库中的令牌ID是否有效。这样既满足你的业务需求,又能解决UserDetailsService缺失的问题。
@Service public class CustomTokenServices extends DefaultTokenServices { private final TokenStore tokenStore; private final YourTokenRepository tokenRepository; // 替换成你存储令牌的数据库Repository @Autowired public CustomTokenServices(TokenStore tokenStore, UserDetailsService userDetailsService, AuthenticationManager authenticationManager, YourTokenRepository tokenRepository) { this.tokenStore = tokenStore; this.tokenRepository = tokenRepository; // 配置TokenServices的核心依赖 setTokenStore(tokenStore); setSupportRefreshToken(true); setReuseRefreshToken(false); // 每次刷新生成新的刷新令牌,根据你的需求调整 setUserDetailsService(userDetailsService); // 关联你的UserDetailsService setAuthenticationManager(authenticationManager); } @Override public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) throws AuthenticationException { // 第一步:从TokenStore读取刷新令牌 OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue); if (refreshToken == null) { throw new InvalidTokenException("无效的刷新令牌: " + refreshTokenValue); } // 第二步:校验数据库中的令牌ID是否有效 String tokenId = refreshToken.getValue(); // 替换成你存储的令牌ID字段 Optional<YourTokenEntity> storedToken = tokenRepository.findByTokenId(tokenId); if (!storedToken.isPresent() || !storedToken.get().isValid()) { throw new InvalidTokenException("刷新令牌已失效或被撤销: " + refreshTokenValue); } // 第三步:执行默认的刷新令牌逻辑 return super.refreshAccessToken(refreshTokenValue, tokenRequest); } }
2. 在授权服务器配置中关联自定义TokenServices
修改AuthorizationServerConfig,将自定义的CustomTokenServices注入到端点配置中,替换默认的TokenServices:
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { // 省略已有的字段... @Autowired private CustomTokenServices customTokenServices; @Override public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer, accessTokenConverter)); endpoints.tokenStore(tokenStore) .tokenEnhancer(tokenEnhancerChain) .authenticationManager(this.authenticationManagerBean) .tokenServices(customTokenServices); // 绑定自定义TokenServices } }
3. 确保UserDetailsService被正确暴露为Bean
在WebSecurityConfig中,将你的UserDetailsService声明为Spring Bean,确保授权服务器能正确注入:
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 替换成你的UserDetailsService实现类 @Bean public UserDetailsService userDetailsService() { return new YourCustomUserDetailsService(); } // 省略其他已有配置... }
关于启动时的异常
你看到的No authenticationProviders and no parentAuthenticationManager defined异常,是因为WebSecurityConfigurerAdapter的内部初始化顺序问题——在configureGlobal执行前,ProviderManager暂时没有配置Provider,但后续配置会生效,所以密码登录功能正常。这个异常可以忽略,或者你可以通过提前初始化Provider来避免,但不会影响功能。
额外说明
- 如果你希望实现「同一用户仅能在一台设备登录」,除了校验刷新令牌ID,还需要在用户登录成功后,清除该用户之前的所有有效令牌,这样旧的刷新令牌就会失效。
setReuseRefreshToken(false)会让每次刷新都生成新的刷新令牌,更符合「单设备登录」的需求,旧的刷新令牌会被标记为无效。
内容的提问来源于stack exchange,提问作者CChong

