最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

Efficient Token Refresh with OAuth2 Client Credentials Flow in Spring Boot 3.4 using RestClient - Stack Overflow

programmeradmin0浏览0评论

I’m currently using Spring Boot 3.4 and Java 21 and trying to integrate RestClient with OAuth2 client credentials flow. I’ve come across a tricky scenario and would appreciate any guidance:

Problem Description

I have a bearer token generation URL and a protected API endpoint. I use RestClient with client credentials authorization-grant-type, supplying my client ID and secret. It works perfectly for a single session. However, when someone regenerates the token (via a portal), the old token becomes invalid immediately, and I encounter issues.

Current Challenge

My app keeps using the old token to call APIs, which results in 401 Unauthorized errors. To mitigate this, I tried using WebClient with an exchange filter function to detect 401 errors, fetch a new token, and retry the API call. The retry works for the first call but does not persist the new token for subsequent API calls. The app ends up calling the token generation endpoint every time, which is inefficient.

I have tried with the RestClient before and it dosen't work and I have gone back to WebClient Still the issue exist.

Below code belongs to WebClient.


import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.Base64;
import java.util.Objects;

@Slf4j
@Configuration
public class Oauth2WebClientConfig {

   private static final String TRACE_ID = "TRACE_ID";
   private final Environment env;

   @Autowired
   public Oauth2WebClientConfig(Environment env) {
       this.env = env;
   }
   // == Oauth2 Configuration ==

   // == Oauth2 Configuration ==
   @Bean
   ReactiveClientRegistrationRepository clientRegistration() {
       ClientRegistration clientRegistration = ClientRegistration
               .withRegistrationId("custom")
               .tokenUri(env.getProperty("token-uri"))
               .clientId(env.getProperty("client-id"))
               .clientSecret(env.getProperty("client-secret"))
               .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
               .scope(env.getProperty("scope"))
               .build();
       return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
   }

   @Bean
   ReactiveOAuth2AuthorizedClientService authorizedClientService() {
       return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistration());
   }

   // == WebFlux Configuration ==
   @Bean
   WebClient webClient(ReactiveClientRegistrationRepository clientRegistration, ReactiveOAuth2AuthorizedClientService authorizedClientService) {
       ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
               new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistration, authorizedClientService));
       oauth.setDefaultClientRegistrationId("custom");

       // @formatter:off
       return WebClient.builder()
               .filter(oauth)
               .filters(exchangeFilterFunctions -> {
                   exchangeFilterFunctions.add(renewTokenFilter());
                   exchangeFilterFunctions.add(logRequest());
                   exchangeFilterFunctions.add(logResponse());
               })
               .build();
       // @formatter:on
   }

   // == Renew Token if expired filter ==
   private ExchangeFilterFunction renewTokenFilter() {
       return (request, next) -> next.exchange(request).flatMap(response -> {
           if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
               // @formatter:off
               return response.releaseBody()
                       .then(WebClient.create().post()
                               .uri(Objects.requireNonNull(env.getProperty("token-uri")))
                               .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString((env.getProperty("client-id") + ":" + env.getProperty("client-secret")).getBytes()))
                               .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
                               .body(BodyInserters.fromFormData(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()))
                               .retrieve()
                               .bodyToMono(String.class))
                       .flatMap(token -> {
                           JSONObject tokenResponse = new JSONObject(token);
                           log.info("new token : {} TRACE_ID : {}", tokenResponse.getString("access_token"), Objects.requireNonNull(request.headers().get(TRACE_ID)).getFirst());
                           ClientRequest newRequest = ClientRequest
                                   .from(request)
                                   .headers(headers -> headers.remove(HttpHeaders.AUTHORIZATION))
                                   .header(HttpHeaders.AUTHORIZATION, "Bearer " + tokenResponse.getString("access_token"))
                                   .build();

                           return next.exchange(newRequest);
                       });
               // @formatter:on
           } else {
               return Mono.just(response);
           }
       });
   }

   // == Log Request ==
   private ExchangeFilterFunction logRequest() {
       return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
           StringBuilder sb = new StringBuilder("Request: \n")
                   .append("Method : ")
                   .append(clientRequest.method())
                   .append(" ")
                   .append("Headers : ")
                   .append(clientRequest.headers())
                   .append(" ")
                   .append("URL : ")
                   .append(clientRequest.url());
           clientRequest
                   .headers()
                   .forEach((name, values) -> values.forEach(value -> sb
                           .append("\n")
                           .append(name)
                           .append(":")
                           .append(value)));
           log.info(sb.toString());

           return Mono.just(clientRequest);
       });
   }

   // == Log Response ==
   private ExchangeFilterFunction logResponse() {
       return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
           StringBuilder sb = new StringBuilder("Response: \n")
                   .append("StatusCode: ")
                   .append(clientResponse.statusCode().value())
                   .append(" ")
                   .append("Headers : ")
                   .append(clientResponse.headers());
           clientResponse
                   .headers()
                   .asHttpHeaders()
                   .forEach((key, value1) -> value1.forEach(value -> sb
                           .append("\n")
                           .append(key)
                           .append(":")
                           .append(value)));
           log.info(sb.toString());
           return Mono.just(clientResponse);
       });
   }
   // == WebFlux Configuration ==
}

Questions

  1. Is it possible to persist the refreshed token efficiently using either RestClient or WebClient, so the app doesn’t repeatedly fetch the token (I prefer RestClient)?
  2. Are there any Spring best practices or patterns to handle this scenario?

I’m currently using Spring Boot 3.4 and Java 21 and trying to integrate RestClient with OAuth2 client credentials flow. I’ve come across a tricky scenario and would appreciate any guidance:

Problem Description

I have a bearer token generation URL and a protected API endpoint. I use RestClient with client credentials authorization-grant-type, supplying my client ID and secret. It works perfectly for a single session. However, when someone regenerates the token (via a portal), the old token becomes invalid immediately, and I encounter issues.

Current Challenge

My app keeps using the old token to call APIs, which results in 401 Unauthorized errors. To mitigate this, I tried using WebClient with an exchange filter function to detect 401 errors, fetch a new token, and retry the API call. The retry works for the first call but does not persist the new token for subsequent API calls. The app ends up calling the token generation endpoint every time, which is inefficient.

I have tried with the RestClient before and it dosen't work and I have gone back to WebClient Still the issue exist.

Below code belongs to WebClient.


import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.Base64;
import java.util.Objects;

@Slf4j
@Configuration
public class Oauth2WebClientConfig {

   private static final String TRACE_ID = "TRACE_ID";
   private final Environment env;

   @Autowired
   public Oauth2WebClientConfig(Environment env) {
       this.env = env;
   }
   // == Oauth2 Configuration ==

   // == Oauth2 Configuration ==
   @Bean
   ReactiveClientRegistrationRepository clientRegistration() {
       ClientRegistration clientRegistration = ClientRegistration
               .withRegistrationId("custom")
               .tokenUri(env.getProperty("token-uri"))
               .clientId(env.getProperty("client-id"))
               .clientSecret(env.getProperty("client-secret"))
               .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
               .scope(env.getProperty("scope"))
               .build();
       return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
   }

   @Bean
   ReactiveOAuth2AuthorizedClientService authorizedClientService() {
       return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistration());
   }

   // == WebFlux Configuration ==
   @Bean
   WebClient webClient(ReactiveClientRegistrationRepository clientRegistration, ReactiveOAuth2AuthorizedClientService authorizedClientService) {
       ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
               new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistration, authorizedClientService));
       oauth.setDefaultClientRegistrationId("custom");

       // @formatter:off
       return WebClient.builder()
               .filter(oauth)
               .filters(exchangeFilterFunctions -> {
                   exchangeFilterFunctions.add(renewTokenFilter());
                   exchangeFilterFunctions.add(logRequest());
                   exchangeFilterFunctions.add(logResponse());
               })
               .build();
       // @formatter:on
   }

   // == Renew Token if expired filter ==
   private ExchangeFilterFunction renewTokenFilter() {
       return (request, next) -> next.exchange(request).flatMap(response -> {
           if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
               // @formatter:off
               return response.releaseBody()
                       .then(WebClient.create().post()
                               .uri(Objects.requireNonNull(env.getProperty("token-uri")))
                               .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString((env.getProperty("client-id") + ":" + env.getProperty("client-secret")).getBytes()))
                               .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
                               .body(BodyInserters.fromFormData(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()))
                               .retrieve()
                               .bodyToMono(String.class))
                       .flatMap(token -> {
                           JSONObject tokenResponse = new JSONObject(token);
                           log.info("new token : {} TRACE_ID : {}", tokenResponse.getString("access_token"), Objects.requireNonNull(request.headers().get(TRACE_ID)).getFirst());
                           ClientRequest newRequest = ClientRequest
                                   .from(request)
                                   .headers(headers -> headers.remove(HttpHeaders.AUTHORIZATION))
                                   .header(HttpHeaders.AUTHORIZATION, "Bearer " + tokenResponse.getString("access_token"))
                                   .build();

                           return next.exchange(newRequest);
                       });
               // @formatter:on
           } else {
               return Mono.just(response);
           }
       });
   }

   // == Log Request ==
   private ExchangeFilterFunction logRequest() {
       return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
           StringBuilder sb = new StringBuilder("Request: \n")
                   .append("Method : ")
                   .append(clientRequest.method())
                   .append(" ")
                   .append("Headers : ")
                   .append(clientRequest.headers())
                   .append(" ")
                   .append("URL : ")
                   .append(clientRequest.url());
           clientRequest
                   .headers()
                   .forEach((name, values) -> values.forEach(value -> sb
                           .append("\n")
                           .append(name)
                           .append(":")
                           .append(value)));
           log.info(sb.toString());

           return Mono.just(clientRequest);
       });
   }

   // == Log Response ==
   private ExchangeFilterFunction logResponse() {
       return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
           StringBuilder sb = new StringBuilder("Response: \n")
                   .append("StatusCode: ")
                   .append(clientResponse.statusCode().value())
                   .append(" ")
                   .append("Headers : ")
                   .append(clientResponse.headers());
           clientResponse
                   .headers()
                   .asHttpHeaders()
                   .forEach((key, value1) -> value1.forEach(value -> sb
                           .append("\n")
                           .append(key)
                           .append(":")
                           .append(value)));
           log.info(sb.toString());
           return Mono.just(clientResponse);
       });
   }
   // == WebFlux Configuration ==
}

Questions

  1. Is it possible to persist the refreshed token efficiently using either RestClient or WebClient, so the app doesn’t repeatedly fetch the token (I prefer RestClient)?
  2. Are there any Spring best practices or patterns to handle this scenario?
Share Improve this question edited Feb 6 at 18:18 dur 17k26 gold badges88 silver badges143 bronze badges asked Feb 5 at 17:18 praneethpraneeth 2251 gold badge4 silver badges16 bronze badges 5
  • So you have a RestClient with an OAuth2ClientHttpRequestInterceptor with an OAuth2AuthorizedClientManager? If you can show us some code, that would be nice. BR – Roar S. Commented Feb 5 at 18:12
  • I have added the code Belongs to WebClient I have tried with OAuth2ClientHttpRequestInterceptor but did not succeed. – praneeth Commented Feb 5 at 18:38
  • I'll create a setup here with RestClient and test it before posting an answer. – Roar S. Commented Feb 5 at 18:41
  • 1 Thanks, @Roar S! Much appreciated. This has been a real pain point for me. I really appreciate your prompt response and support! – praneeth Commented Feb 5 at 18:46
  • You got your self a clean setup, but you should look at the answer from @ch4mp as well, he's very into this. BR – Roar S. Commented Feb 5 at 20:42
Add a comment  | 

2 Answers 2

Reset to default 1

You should configure your RestClient bean with an OAuth2AuthorizationFailureHandler.

Samples in the Spring doc linked above and in this other answer (the subject of which is proxy configuration, but all RestClient beans in this answer are configured with an OAuth2AuthorizationFailureHandler, including those auto-configured by "my" starter).

This RestClient configuration using client_credentials seems to work fine between restarts of the authorization server. Also added OAuth2AuthorizationFailureHandler as suggested from @ch4mp.

Test setup:

  • Local authorization server
  • Local resource server
  • Modified version of this Spring Boot client

RestClientConfig

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2AuthorizationFailureHandler;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
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.client.OAuth2ClientHttpRequestInterceptor;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {

    @Bean
    public RestClient restClient(
            RestClient.Builder builder,
            @Value("${messages.backend-base-uri}") String baseUri,
            OAuth2AuthorizedClientManager authorizedClientManager,
            OAuth2AuthorizedClientRepository authorizedClientRepository) {

        var interceptor = new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);

        // from @ch4mp in an earlier SO-question 
        interceptor.setClientRegistrationIdResolver(request -> "messaging-client-credentials");

        OAuth2AuthorizationFailureHandler authorizationFailureHandler =
                OAuth2ClientHttpRequestInterceptor.authorizationFailureHandler(authorizedClientRepository);

        interceptor.setAuthorizationFailureHandler(authorizationFailureHandler);

        return builder
                .baseUrl(baseUri)
                .requestInterceptor(interceptor)
                .build();
    }

    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientRepository authorizedClientRepository) {

        var authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
                .authorizationCode() // can be removed in your case
                .clientCredentials()
                .refreshToken()
                .build();

        var authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
                clientRegistrationRepository,
                authorizedClientRepository);

        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
        return authorizedClientManager;
    }
}

application.yml Replace with your values.

  security:
    oauth2:
      client:
        registration:
          messaging-client-credentials:
            provider: spring
            client-id: next-app-client
            client-secret: secret
            authorization-grant-type: client_credentials
            client-name: messaging-client-credentials
        provider:
          spring:
            issuer-uri: http://localhost:9000/auth-server

Example controller

@Controller
public class MessagesController {
    private final RestClient restClient;

    public MessagesController(RestClient restClient) {
        this.restClient = restClient;
    }

    @GetMapping(value = "/messages-restclient-attrs")
    public String messagesUsingRestClientWithAttributes(Model model) {

        String[] messages = restClient
                .get()
                .uri("/messages")
                .retrieve()
                .body(String[].class);

        model.addAttribute("messages", messages);

        return "index";
    }
}

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论