Description
Spring Cloud offers some auto-configuration for its @FeignClient
which entered maintenance mode in favor of RestClient
and WebClient
used with HttpServiceProxyFactory
for @HttpExchange
.
The new solution provides similar declarative REST client features, at the price of quite some Java conf especially when request authorization is involved - which should almost always be the case.
I experienced @HttpExchange
proxies auto-configuration using application properties in this starter of mine and I think that the features I implemented are worth integrating into the "official" framework because they greatly improve developers' experience.
Sample
Use case
Let's consider the pool of oauth2ResourceServer
microservices from this sample repository.
the 3 different declinations of the MicroserviceChouette*Application
call:
MicroserviceMachinApplication
on behalf of the resource owner at the origin of the requests: the requests are authorized re-using theBearer
token in the security-context (MicroserviceChouetteApplication
is a resource server, so the request it processes already is authorized with aBearer
token).MicroserviceBiduleApplication
in their own names: a newBearer
token is acquired using client-credentials flow.
The MicroserviceMachinApplication
exposes an OpenAPI document from which we can generate the following:
@HttpExchange
public interface MachinApi {
@GetExchange("/truc")
String getTruc();
}
The MicroserviceBiduleApplication
exposes an OpenAPI document from which we can generate the following:
@HttpExchange
public interface BiduleApi {
@GetExchange("/chose")
String getChose();
}
MicroserviceChouetteApplication
collaborates with the two REST APIs above as follows:
@RestController
@RequiredArgsConstructor
public class ChouetteController {
private final MachinApi machinApi;
private final BiduleApi biduleApi;
@GetMapping("/chouette-truc")
public String getChouetteTruc() {
return machinApi.getTruc();
}
@GetMapping("/chouette-chose")
public String getChouetteChose() {
return biduleApi.getChose();
}
}
This requires implementations for MachinApi
and BiduleApi
to be exposed as beans, internally using a RestClient
or WebClient
instance to retrieve REST resources from other services - authorizing these requests with Bearer
tokens.
Common security configuration
issuer: https://oidc.c4-soft.com/auth/realms/rest-showcase
bidule-api-port: 8081
machin-api-port: 8082
server:
port: 8083
spring:
application:
name: bidule-api
security:
oauth2:
client:
provider:
sso:
issuer-uri: ${issuer}
registration:
bidule-registration:
provider: sso
authorization-grant-type: client_credentials
client-id: chouette-api
client-secret: change-me
scope: openid
resourceserver:
jwt:
issuer-uri: ${issuer}
REST configuration with just "official" 3.4.0-RC1
starters
I believe that we can hardly be more synthetic than the following for having MachinApi
and BiduleApi
implementations generated by HttpServiceProxyFactory
, using RestClient
instances configured with the required ClientHttpRequestInterceptor
s:
bidule-base-uri: http://localhost:${bidule-api-port}
machin-base-uri: http://localhost:${machin-api-port}
@Configuration
public class RestConfiguration {
@Bean
RestClient machinClient(@Value("${machin-base-uri}") URI machinBaseUri) {
return RestClient.builder().baseUrl(machinBaseUri)
.requestInterceptor(forwardingClientHttpRequestInterceptor()).build();
}
@Bean
MachinApi machinApi(RestClient machinClient) {
return HttpServiceProxyFactory.builderFor(RestClientAdapter.create(machinClient)).build()
.createClient(MachinApi.class);
}
@Bean
RestClient biduleClient(@Value("${bidule-base-uri}") URI biduleBaseUri,
OAuth2AuthorizedClientManager authorizedClientManager,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
return RestClient.builder().baseUrl(biduleBaseUri)
.requestInterceptor(registrationClientHttpRequestInterceptor(authorizedClientManager,
authorizedClientRepository, "bidule-registration"))
.build();
}
@Bean
BiduleApi biduleApi(RestClient biduleClient) {
return HttpServiceProxyFactory.builderFor(RestClientAdapter.create(biduleClient)).build()
.createClient(BiduleApi.class);
}
ClientHttpRequestInterceptor forwardingClientHttpRequestInterceptor() {
return (HttpRequest request, byte[] body, ClientHttpRequestExecution execution) -> {
final var auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof AbstractOAuth2Token oauth2Token) {
request.getHeaders().setBearerAuth(oauth2Token.getTokenValue());
}
return execution.execute(request, body);
};
}
ClientHttpRequestInterceptor registrationClientHttpRequestInterceptor(
OAuth2AuthorizedClientManager authorizedClientManager,
OAuth2AuthorizedClientRepository authorizedClientRepository, String registrationId) {
final var interceptor = new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
interceptor.setClientRegistrationIdResolver((HttpRequest request) -> registrationId);
interceptor.setAuthorizationFailureHandler(
OAuth2ClientHttpRequestInterceptor.authorizationFailureHandler(authorizedClientRepository));
return interceptor;
}
}
Things get even more complicated if the ClientHttpRequestFactory
needs configuration for connect timeout, read timeout, or HTTP or SOCKS proxy (reach a remote service like Google API).
REST configuration with spring-addons-starter-rest
The RestConfiguration
becomes:
com:
c4-soft:
springaddons:
rest:
client:
machin-client:
base-url: ${machin-base-uri}
authorization:
oauth2:
forward-bearer: true
bidule-client:
base-url: ${bidule-base-uri}
authorization:
oauth2:
oauth2-registration-id: bidule-registration
@Configuration
public class RestConfiguration {
@Bean
BiduleApi biduleApi(RestClient biduleClient) throws Exception {
return new RestClientHttpExchangeProxyFactoryBean<>(BiduleApi.class, biduleClient).getObject();
}
@Bean
MachinApi machinApi(RestClient machinClient) throws Exception {
return new RestClientHttpExchangeProxyFactoryBean<>(MachinApi.class, machinClient).getObject();
}
}
Features
What is already implemented:
- Expose RestClient or WebClient named beans preconfigured with:
- A base URI that is likely to change from one deployment to another.
- Request authorization with a choice of Basic and Bearer, and for the latter, the choice of forwarding the token in the security context of a resource server, or obtained using an OAuth2 client registration ID.
- SetProxy-Authorization
header and configure aClientHttpRequestFactory
for HTTP or SOCKS proxy. Enabled by default if the standardHTTP_PROXY
andNO_PROXY
environment variables or custom application properties are set, but can be disabled on any auto-configured client. - Clients are
RestClient
by default in servlets andWebClients
in Webflux apps, but any client can be switched toWebClient
in servlets. - Choice to expose the builder instead of an already built
RestClient
orWebClient
. This can be useful when some more configuration is needed than what the starter implements. - The default REST client bean name is the camelCase version of its ID in properties (with
Builder
suffix ifexpose-builder=true
). A custom name can be defined in properties
Room for improvement: remove the need for the generated @HttpExchange
proxies beans definition in the RestConfiguration
. I haven't found how to properly post-process the BeanDefinitionRegistry
. The additional properties could look like the following:
service:
machin-api:
client-bean-name: machinClient
http-exchange-class: com.c4soft.showcase.rest.MachinApi
bidule-api:
client-bean-name: biduleClient
http-exchange-class: com.c4soft.showcase.rest.BiduleApi
The great point of using a client bean name (rather than a key under the client
properties), is that it allows to use any REST client, which could be a bean exposed using an auto-configured builder or a completely hand-crafted one.
Additional context
I already asked for this in Spring Security issues. @sjohnr wrote that such auto-configuration requests better fit here, and also that this shouldn't be implemented in the "official" framework because this would be "programming with yaml".
I have a different opinion about such auto-configuration. To me, it is about:
- Meeting the DRY principle. I don't want to repeat such Java configuration in each of my resource servers collaborating with other REST API(s).
- Limiting static configuration in application code.
- Flattening the learning curve and improving developer productivity. IDEs autocompletion and documentation features help much more when writing YAML configuration than when implementing Java configuration as the one above, which is far from trivial: we have to remember the name of several classes, which factory to use, which default property to override, which configuration trick is available in servlets or reactive apps, etc.
- Reducing the impact of breaking changes with future Spring Security and Spring Web(flux) versions.
My starter is just fine for me and the (very) few teams getting to know it and accepting to use it, but I'm sure that many more would be glad to benefit from such auto-configuration using just the official Boot starters.
Activity