You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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方案)均未解决。

咨询问题

  1. 现有代码是否存在问题?Spring版本迭代快,担心配置过时
  2. 如何正确获取Refresh Token?曾尝试在oauth2login()中配置转换器但失败,部分示例代码无法编译
  3. 低优先级:代码需封装为库,目前需在库中创建带@Configuration、@ComponentScan的MyLibReference类,主应用类通过@Import导入。如何实现主应用引入Maven依赖后自动扫描库组件?
  4. 低优先级:如何将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的自动配置机制:

  1. 在库的src/main/resources/META-INF目录下创建spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件
  2. 文件中写入你的库配置类的全限定名,比如:
com.yourpackage.lib.config.SecurityConfig
com.yourpackage.lib.config.OtherConfig

这样主应用引入Maven依赖后,Spring Boot会自动加载这些配置类,无需手动导入。

4. 库中存储Provider配置

可以通过自动配置属性实现:

  1. 在库中定义配置属性类:
@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
}
  1. 创建自动配置类,绑定属性并注册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);
    }
}
  1. 把这个自动配置类加入到spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中,主应用可以通过application.yaml覆盖库中的默认配置,也可以直接使用库预设的配置。

内容的提问来源于stack exchange,提问作者Guigui Lechat

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 17:54:53