Spring OAuth2登录获取Refresh Token及相关配置疑问
需求场景
- 用户访问站点进入受保护页面时,自动重定向至OAuth2登录页面
- 服务器接收新连接后,按需存储Refresh Token
- 通过
@Scheduled定时任务,使用Refresh Token从远程服务器拉取数据并存入数据库
已完成配置
已完成OAuth2基础配置:
- 在
application.yaml中配置了spring.security.oauth2.client.provider及对应客户端注册信息 - 实现了Security配置Bean:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/", "/index").permitAll() .anyRequest().authenticated()) .oauth2Login(Customizer.withDefaults()); return http.build(); } }
当前访问localhost:8080/正常,访问受保护页面(如localhost:8080/user)会正确重定向到OAuth登录,登录后可正常访问资源。
另外实现了自定义OAuth2UserService用于拦截连接:
@Service @RequiredArgsConstructor(onConstructor = @__(@Lazy)) public class Oauth2UserService extends DefaultOAuth2UserService { @Override public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) { OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest); return processOAuth2User(oAuth2UserRequest, oAuth2User); } private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) { System.out.println("user attributes : " + oAuth2User.getAttributes()); System.out.println("access token is " + oAuth2UserRequest.getAccessToken().getTokenValue()); System.out.println( "refresh token is : " + oAuth2UserRequest.getAdditionalParameters().get(OAuth2ParameterNames.REFRESH_TOKEN)); System.out.println( "additional parameters are : " + oAuth2UserRequest.getAdditionalParameters()); return oAuth2User; } }
当前问题
仅能获取到AccessToken(有效期短,不适合长期任务),Refresh Token始终为null,additionalParameters是空Map,尝试过多种配置(包括Stack Overflow方案)均未解决。
咨询问题
- 现有代码是否存在问题?Spring版本迭代快,担心配置过时
- 如何正确获取Refresh Token?曾尝试在
oauth2login()中配置转换器但失败,部分示例代码无法编译 - 低优先级:代码需封装为库,目前需在库中创建带
@Configuration、@ComponentScan的MyLibReference类,主应用类通过@Import导入。如何实现主应用引入Maven依赖后自动扫描库组件? - 低优先级:如何将Provider配置存储到库中,而非写入主应用的
application.yaml?
临时可行方案
调试后实现了一个可行方案,在SecurityConfig中添加以下代码:
@Configuration @EnableWebSecurity public class SecurityConfig { // filterChain无改动,无需调整 @Bean public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() { DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient(); OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); tokenResponseHttpMessageConverter.setAccessTokenResponseConverter(new CustomTokenResponseConverter()); RestTemplate restTemplate = new RestTemplate(Arrays.asList( new FormHttpMessageConverter(), tokenResponseHttpMessageConverter)); restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); accessTokenResponseClient.setRestOperations(restTemplate); return accessTokenResponseClient; } public static class CustomTokenResponseConverter implements Converter<Map<String, Object>, OAuth2AccessTokenResponse> { @Override public OAuth2AccessTokenResponse convert(Map<String, Object> tokenResponseParameters) { String accessToken = (String) tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN); String refreshToken = (String) tokenResponseParameters.get(OAuth2ParameterNames.REFRESH_TOKEN); Object expiresInObj = tokenResponseParameters.get(OAuth2ParameterNames.EXPIRES_IN); long expiresIn = ((Number) expiresInObj).longValue(); Set<String> scopes = Collections.emptySet(); if (tokenResponseParameters.containsKey(OAuth2ParameterNames.SCOPE)) { String scope = (String) tokenResponseParameters.get(OAuth2ParameterNames.SCOPE); scopes = Arrays.stream(StringUtils.delimitedListToStringArray(scope, " ")) .collect(Collectors.toSet()); } return OAuth2AccessTokenResponse.withToken(accessToken) .tokenType(OAuth2AccessToken.TokenType.BEARER) .expiresIn(expiresIn) .scopes(scopes) .refreshToken(refreshToken) .additionalParameters(Map.of(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken)) .build(); } } }
原方案中把expiresIn按字符串解析,但当前框架中该值是数字类型,导致解析失败。不确定这是否是最优方案,担心手动配置会影响Spring自动配置。
1. 现有代码问题排查
你的基础Security配置和自定义OAuth2UserService本身没有过时问题,核心问题出在Refresh Token的获取逻辑上:
- 默认的
DefaultAuthorizationCodeTokenResponseClient在处理token响应时,可能没有正确解析Refresh Token(尤其是当授权服务器返回的响应格式和Spring默认预期有差异时,比如expires_in是数字而非字符串) - 你在
OAuth2UserService中从oAuth2UserRequest.getAdditionalParameters()拿Refresh Token的方式不对,默认情况下Refresh Token不会被放到additionalParameters里,而是需要从OAuth2AccessTokenResponse中获取
2. 正确获取Refresh Token的最优方案
你的临时方案思路是对的,但可以优化得更贴合Spring自动配置,避免完全手动构建RestTemplate:
方案优化:复用Spring自动配置的RestTemplate
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient( RestTemplateBuilder restTemplateBuilder) { DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient(); // 自定义响应转换器,处理数字类型的expires_in OAuth2AccessTokenResponseHttpMessageConverter converter = new OAuth2AccessTokenResponseHttpMessageConverter(); converter.setAccessTokenResponseConverter(new CustomTokenResponseConverter()); // 用Spring自动配置的RestTemplateBuilder构建,继承默认配置 RestTemplate restTemplate = restTemplateBuilder .additionalMessageConverters(new FormHttpMessageConverter(), converter) .errorHandler(new OAuth2ErrorResponseErrorHandler()) .build(); tokenResponseClient.setRestOperations(restTemplate); return tokenResponseClient; } // 自定义转换器,处理expires_in为数字的情况 public static class CustomTokenResponseConverter implements Converter<Map<String, Object>, OAuth2AccessTokenResponse> { @Override public OAuth2AccessTokenResponse convert(Map<String, Object> params) { String accessToken = (String) params.get(OAuth2ParameterNames.ACCESS_TOKEN); String refreshToken = (String) params.get(OAuth2ParameterNames.REFRESH_TOKEN); long expiresIn = ((Number) params.getOrDefault(OAuth2ParameterNames.EXPIRES_IN, 0L)).longValue(); Set<String> scopes = params.containsKey(OAuth2ParameterNames.SCOPE) ? Arrays.stream(((String) params.get(OAuth2ParameterNames.SCOPE)).split(" ")) .collect(Collectors.toSet()) : Collections.emptySet(); return OAuth2AccessTokenResponse.withToken(accessToken) .tokenType(OAuth2AccessToken.TokenType.BEARER) .expiresIn(expiresIn) .scopes(scopes) .refreshToken(refreshToken) .additionalParameters(params) // 保留所有额外参数,而不是只放refresh token .build(); } } // 配置oauth2Login使用自定义的token响应客户端 @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/", "/index").permitAll() .anyRequest().authenticated()) .oauth2Login(oauth2 -> oauth2 .tokenEndpoint(token -> token.accessTokenResponseClient(accessTokenResponseClient(null)))); return http.build(); } }
如何在OAuth2UserService中获取Refresh Token
修改自定义OAuth2UserService,通过OAuth2UserRequest的getAccessToken()拿到OAuth2AccessToken,然后获取Refresh Token:
@Service public class Oauth2UserService extends DefaultOAuth2UserService { @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) { OAuth2User user = super.loadUser(userRequest); // 从OAuth2AccessToken中获取Refresh Token OAuth2AccessToken accessToken = userRequest.getAccessToken(); OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); if (refreshToken != null) { System.out.println("refresh token: " + refreshToken.getTokenValue()); // 这里可以把refresh token存储到数据库 } return user; } }
3. 库组件自动扫描实现
不需要手动创建MyLibReference和@Import,直接用Spring的自动配置机制:
- 在库的
src/main/resources/META-INF目录下创建spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件 - 文件中写入你的库配置类的全限定名,比如:
com.yourpackage.lib.config.SecurityConfig com.yourpackage.lib.config.OtherConfig
这样主应用引入Maven依赖后,Spring Boot会自动加载这些配置类,无需手动导入。
4. 库中存储Provider配置
可以通过自动配置属性实现:
- 在库中定义配置属性类:
@ConfigurationProperties(prefix = "spring.security.oauth2.client.provider.your-provider") public class OAuth2ProviderProperties { private String authorizationUri; private String tokenUri; private String userInfoUri; private String userNameAttributeName; // 生成getter和setter }
- 创建自动配置类,绑定属性并注册Provider:
@Configuration @EnableConfigurationProperties(OAuth2ProviderProperties.class) public class OAuth2ProviderAutoConfig { @Bean public ClientRegistrationRepository clientRegistrationRepository( OAuth2ProviderProperties properties) { // 这里可以结合主应用配置的client信息,或者直接在库中预设默认值 ClientRegistration registration = ClientRegistration.withRegistrationId("your-provider") .authorizationUri(properties.getAuthorizationUri()) .tokenUri(properties.getTokenUri()) .userInfoUri(properties.getUserInfoUri()) .userNameAttributeName(properties.getUserNameAttributeName()) // 其他配置,比如clientId和clientSecret可以让主应用配置,或者库中预设 .clientId("default-client-id") .clientSecret("default-client-secret") .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") .build(); return new InMemoryClientRegistrationRepository(registration); } }
- 把这个自动配置类加入到
spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中,主应用可以通过application.yaml覆盖库中的默认配置,也可以直接使用库预设的配置。
内容的提问来源于stack exchange,提问作者Guigui Lechat

