1
0
Files
spring-security-demo/service-to-service/docs/03-restclient-interceptors.md

3.5 KiB

3. RestClient interceptors

Prev: 2. Three ways to get a token · Next: 4. What a resource server does not validate

OAuth2ClientHttpRequestInterceptor, in org.springframework.security.oauth2.client.web.client, is the framework's answer for a RestClient that needs a token. Its whole public surface:

public OAuth2ClientHttpRequestInterceptor(OAuth2AuthorizedClientManager manager);
public void setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler handler);
public void setClientRegistrationIdResolver(ClientRegistrationIdResolver resolver);
public void setPrincipalResolver(PrincipalResolver resolver);

Wiring it takes one line, and choosing the registration per call takes one more:

RestClient client = builder
    .baseUrl("http://127.0.0.1:8082")
    .requestInterceptor(new OAuth2ClientHttpRequestInterceptor(authorizedClientManager))
    .build();

client.get().uri("/orders")
    .attributes(clientRegistrationId("edge-service"))   // static import
    .retrieve().body(Map.class);

clientRegistrationId(..) is a static method on RequestAttributeClientRegistrationIdResolver. Without it, the default resolver finds nothing and the request goes out unauthenticated.

Which OAuth2AuthorizedClientManager

This is the choice that decides whether the thing works off a request thread.

Manager Storage Needs a request?
DefaultOAuth2AuthorizedClientManager OAuth2AuthorizedClientRepository (session) Yes
AuthorizedClientServiceOAuth2AuthorizedClientManager OAuth2AuthorizedClientService No

For service-to-service calls there is no end user whose authorization is being stored per session, so the second one is right — and it is the one that keeps working from a scheduled task, a message listener or an @Async method.

Getting this wrong produces ClientAuthorizationRequiredException or a silent null in a context that has no HttpServletRequest, which reads like an OAuth problem and is a bean problem.

What it caches, and what it does not

The manager stores the authorized client (access token and, if issued, refresh token) in the OAuth2AuthorizedClientService and reuses it until it is within the clock skew of expiry. So a client_credentials registration does not hit the token endpoint per request. What it does do is re-request on expiry, synchronously, inside whichever call happens to be first — worth knowing when a latency percentile spikes on a period that matches your token lifetime.

The hand-rolled relay, and why it is still reasonable

The relay interceptor in DownstreamClients does not use any of the above:

var authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof JwtAuthenticationToken token) {
    request.getHeaders().setBearerAuth(token.getToken().getTokenValue());
}

That is not a worse version of OAuth2ClientHttpRequestInterceptor; it is a different thing. The interceptor obtains a token under a client registration. This forwards the token already in hand. There is no client registration for "the caller's token", and there should not be. Just do not confuse the two: the hand-rolled one carries the ThreadLocal dependency from chapter 2, and the framework one does not.


Prev: 2. Three ways to get a token · Next: 4. What a resource server does not validate