1
0

Add the service-to-service module

This commit is contained in:
2026-08-28 10:02:47 +05:30
parent cad813e1ae
commit 0fceb2cd4e
37 changed files with 2566 additions and 4 deletions

View File

@@ -0,0 +1,77 @@
# 3. `RestClient` interceptors
*Prev: [2. Three ways to get a token](02-three-ways-to-get-a-token.md) · Next: [4. What a resource server does not validate](04-what-is-not-validated.md)*
`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:
```java
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:
```java
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`](../src/main/java/com/ankurm/s2s/edge/DownstreamClients.java) does not use
any of the above:
```java
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](02-three-ways-to-get-a-token.md) · Next: [4. What a resource server does not validate](04-what-is-not-validated.md)*