RestTemplate to WebClient - spring

I'm migrating usage of RestTemplate to WebClient, following is my existing RestTemplate code,
val restTemplate: RestTemplate
#Throws(KeyStoreException::class, NoSuchAlgorithmException::class, KeyManagementException::class)
get() {
System.setProperty("https.protocols", "TLSv1,TLSv1.1,TLSv1.2")
val sslContext = SSLContexts.custom()
.loadTrustMaterial(null, InsecureTrustStrategy())
.build()
val socketFactory = SSLConnectionSocketFactory(sslContext)
val httpClient = HttpClients.custom()
.setSSLSocketFactory(socketFactory)
.build()
val requestFactory = HttpComponentsClientHttpRequestFactory(httpClient)
return RestTemplate(requestFactory)
}
private class InsecureTrustStrategy: TrustStrategy {
override fun isTrusted(chain: Array<out X509Certificate>?, authType: String?): Boolean = true
}
Following is my WebClient code
val webClient: WebClient
#Throws(KeyStoreException::class, NoSuchAlgorithmException::class, KeyManagementException::class)
get() {
val sslContext = SslContextBuilder.forClient()
.protocols(
"TLSv1",
"TLSv1.1",
"TLSv1.2",
)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build()
val httpClient = HttpClient.create()
.secure {
it.sslContext(sslContext)
}
val connector = ReactorClientHttpConnector(httpClient)
return WebClient.builder()
.clientConnector(connector)
.build()
}
Is my conversion is correct? Have I missed anything?

Related

How I do pass parameter to configuration bean in Kotlin

I have declared OAuthClientConfiguration setting for WebClient using ReactiveClientRegistrationRepository. But I need to pass some params from service (MyService)
WebClientConfiguration defines as follows:
#Configuration
class WebClientConfiguration {
#Bean
fun clientRegistrations(credentials: Credentials):ReactiveClientRegistrationRepository? {
val registration = ClientRegistration
.withRegistrationId("okta")
.tokenUri(access_token)
.clientId(credentials.client_id)
.clientSecret(credentials.client_secret)
.scope(credentials.scope)
.authorizationGrantType(AuthorizationGrantType(authorizationGrantType))
.build()
return InMemoryReactiveClientRegistrationRepository(registration)
}
#Bean
fun webClient(clientRegistrations: ReactiveClientRegistrationRepository?): WebClient? {
val clientService = InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations)
val authorizedClientManager =
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, clientService)
val oauth = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
oauth.setDefaultClientRegistrationId("okta")
return WebClient.builder()
.filter(oauth)
.build()
}
}
And WebClient calling is defined as follows:
#Service
class MyService (private val webClient: WebClient) {
fun doRequest(): String? {
return webClient.get()
.retrieve()
}
}
How I do pass the credentials to the configuration beans

Configure SSL with Webflux Webclient using Apache HttpComponents

I am trying to migrate from restTemplate to webClient.
Everything was fine until I reached restTemplate config with ClientHttpRequestFactory.
I paste here the old and the new codes.
------Old code with restTemplate-------
private HttpComponentsClientHttpRequestFactory buildRequestFactory() {
HttpClientBuilder clientBuilder = HttpClientBuilder.create();
HttpHost proxy = new HttpHost(proxyHost, proxyPort);
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(new AuthScope(proxyHost, proxyPort),
new UsernamePasswordCredentials(proxyUser, proxyPassword));
clientBuilder.useSystemProperties();
clientBuilder.setProxy(proxy);
clientBuilder.setDefaultCredentialsProvider(credsProvider);
clientBuilder.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy());
TrustStrategy acceptingTrustStrategy = new TrustStrategy() {
public boolean isTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
return true;
}
};
SSLContext sslContext = null;
try {
sslContext = org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
throw new ServiceException(GlobalErrorMessage.INTERNAL_SERVER_ERROR);
}
SSLConnectionSocketFactory connectionFactory = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
CloseableHttpClient httpClient = clientBuilder
.setSSLSocketFactory(connectionFactory)
.setRoutePlanner(new DefaultProxyRoutePlanner(proxy) {
#Override
public HttpHost determineProxy(HttpHost target, HttpRequest request, HttpContext context)
throws HttpException {
if (target.getHostName().equals(noproxy)) {
return null;
}
return super.determineProxy(target, request, context);
}
})
.build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
return requestFactory;
}
#Bean(name = "gatewayRestTemplate")
public RestTemplate gatewayRestTemplateConfig() {
RestTemplate restTemplate = new RestTemplate(converters());
restTemplate.setRequestFactory(buildRequestFactory());
return restTemplate;
}
------New code with webClient-------
private ClientHttpConnector buildClientConnector() {
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
org.apache.hc.core5.http.HttpHost proxy = new org.apache.hc.core5.http.HttpHost(proxyHost, proxyPort);
org.apache.hc.client5.http.auth.CredentialsProvider credsProvider = new org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider();
((org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider) credsProvider).setCredentials(new org.apache.hc.client5.http.auth.AuthScope(proxyHost, proxyPort),
new org.apache.hc.client5.http.auth.UsernamePasswordCredentials(proxyUser, proxyPassword.toCharArray()));
clientBuilder.useSystemProperties();
clientBuilder.setProxy(proxy);
clientBuilder.setDefaultCredentialsProvider(credsProvider);
clientBuilder.setProxyAuthenticationStrategy(new DefaultAuthenticationStrategy());
TrustStrategy acceptingTrustStrategy = new TrustStrategy() {
public boolean isTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
return true;
}
};
SSLContext sslContext = null;
try {
sslContext = org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
throw new ServiceException(GlobalErrorMessage.INTERNAL_SERVER_ERROR);
}
org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory connectionFactory =
new org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
org.apache.hc.core5.http.config.Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
// .<org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory>create().register("https", connectionFactory)
.<ConnectionSocketFactory>create().register("https", connectionFactory)
// .register("http", new PlainConnectionSocketFactory())
.build();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
CloseableHttpAsyncClient client = clientBuilder
.setConnectionManager((AsyncClientConnectionManager) connectionManager)
.setRoutePlanner(new org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner(proxy) {
#Override
protected org.apache.hc.core5.http.HttpHost determineProxy(org.apache.hc.core5.http.HttpHost target, org.apache.hc.core5.http.protocol.HttpContext context) throws org.apache.hc.core5.http.HttpException {
if (target.getHostName().equals(noproxy)) {
return null;
}
return super.determineProxy(target, context);
}
})
.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
return connector;
}
#Primary
#Bean(name = "defaultWebClient")
public WebClient defaultWebClientConfig() {
WebClient webClient = WebClient.builder()
.clientConnector(buildClientConnector())
.build();
return webClient;
}
When I run the project, I get this exception:
Caused by: java.lang.ClassCastException: class org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager cannot be cast to class org.apache.hc.client5.http.nio.AsyncClientConnectionManager (org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager and org.apache.hc.client5.http.nio.AsyncClientConnectionManager are in unnamed module of loader 'app')
Based on Migration to Apache HttpClient 5.0 async APIs, I solved my problem. The idea is to use ClientTlsStrategyBuilder when setting sslContext.
private ClientHttpConnector buildClientConnector() {
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
org.apache.hc.core5.http.HttpHost proxy = new org.apache.hc.core5.http.HttpHost(proxyHost, proxyPort);
org.apache.hc.client5.http.auth.CredentialsProvider credsProvider = new org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider();
((org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider) credsProvider).setCredentials(new org.apache.hc.client5.http.auth.AuthScope(proxyHost, proxyPort),
new org.apache.hc.client5.http.auth.UsernamePasswordCredentials(proxyUser, proxyPassword.toCharArray()));
clientBuilder.useSystemProperties();
clientBuilder.setProxy(proxy);
clientBuilder.setDefaultCredentialsProvider(credsProvider);
clientBuilder.setProxyAuthenticationStrategy(new DefaultAuthenticationStrategy());
TrustStrategy acceptingTrustStrategy = (x509Certificates, s) -> true;
SSLContext sslContext;
try {
sslContext = org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
throw new ServiceException(GlobalErrorMessage.INTERNAL_SERVER_ERROR);
}
PoolingAsyncClientConnectionManager connectionManager = PoolingAsyncClientConnectionManagerBuilder.create()
.setTlsStrategy(ClientTlsStrategyBuilder.create()
.setSslContext(sslContext)
.setHostnameVerifier(new NoopHostnameVerifier())
.build())
.build();
CloseableHttpAsyncClient client = clientBuilder
.setConnectionManager(connectionManager)
.setRoutePlanner(new org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner(proxy) {
#Override
protected org.apache.hc.core5.http.HttpHost determineProxy(org.apache.hc.core5.http.HttpHost target, org.apache.hc.core5.http.protocol.HttpContext context) throws org.apache.hc.core5.http.HttpException {
if (target.getHostName().equals(noproxy)) {
return null;
}
return super.determineProxy(target, context);
}
})
.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
return connector;
}
#Primary
#Bean(name = "defaultWebClient")
public WebClient defaultWebClientConfig() {
WebClient webClient = WebClient.builder()
.clientConnector(buildClientConnector())
.build();
return webClient;
}
If you want to use HttpClient connector. Please use below code for
webclient. The above answers any of them not worked, below solution is
working fine for me.
SslContext sslContext = SslContextBuilder
.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
HttpClient httpClient = HttpClient.create().secure(t ->
t.sslContext(sslContext) );
WebClient webClient = WebClient.builder()
.baseUrl("any-url")
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();

Spring boot using https only - Test failing

I have spring boot - angular application
My server port is 1234 (example).
But now I have setup ssl
Steps:
created self signed cert using keytool and added my p12 cert file into resources folder
updated application.properties
server.ssl.key-store-type=PKCS12
server.ssl.key-store=classpath:myfile.p12
server.ssl.key-store-password=somepasswordfromenv
server.ssl.key-alias=myalias
server.ssl.enabled=true
http.port=8080
server.port=8443
Started app and tested
#SpringBootTest(classes = AppWithoutBeansApplication.class, webEnvironment = WebEnvironment.DEFINED_PORT)
class AppControllerTest {
#Value("${server.ssl.key-store}")
private Resource trustStore;
#Value("${server.ssl.key-store-password}")
private String trustStorePassword;
#Test
public void givenAcceptingAllCertificatesUsing4_4_whenUsingRestTemplate_thenCorrect()
throws ClientProtocolException, IOException {
String urlOverHttps = "https://localhost:8443/";
CloseableHttpClient httpClient
= HttpClients.custom()
.setSSLHostnameVerifier(new NoopHostnameVerifier())
.build();
HttpComponentsClientHttpRequestFactory requestFactory
= new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
ResponseEntity<String> response
= new RestTemplate(requestFactory).exchange(
urlOverHttps, HttpMethod.GET, null, String.class);
assertThat(response.getStatusCode().value(), equalTo(200));
}
...
Error:
org.springframework.web.client.ResourceAccessException: I/O error on GET request for "https://localhost:8443/": Certificate for doesn't match any of the subject alternative names: []; nested exception is javax.net.ssl.SSLPeerUnverifiedException: Certificate for doesn't match any of the subject alternative names: []
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:746)
Don't understand how my integration test works
All answers looked similar, but this one worked
Ignore SSL certificate validation when using Spring RestTemplate
#Bean
public RestTemplate restTemplate() throws GeneralSecurityException {
TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("https", sslsf).register("http", new PlainConnectionSocketFactory()).build();
BasicHttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(
socketFactoryRegistry);
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
.setConnectionManager(connectionManager).build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);
return restTemplate;
}
Looks like this part made the difference
*Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("https", sslsf).register("http", new PlainConnectionSocketFactory()).build();*
DO NOT USE THIS
// #Bean
// public RestTemplate nonsslrestTemplate() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {
// TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
// SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
// SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);
// CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(csf).build();
// HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
// requestFactory.setHttpClient(httpClient);
// return new RestTemplate(requestFactory);
// }

Spring RestTemplate certificate 403 Forbidden: [no body]

I use p12 certificate with RestTemplate to call an external API.
RestTemplate:
final SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(keyStoreFile.getURL(), keyPassword.toCharArray(), (X509Certificate[] chain, String authType) -> true)
.build();
final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext, INSTANCE);
final HttpClient httpClient = custom()
.setSSLSocketFactory(socketFactory)
.setMaxConnTotal(1000)
.setMaxConnPerRoute(40)
.build();
final HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(requestFactory));
And the call:
HttpEntity<String> entity = new HttpEntity<>(httpHeaders);
final ResponseEntity<MyList> response = restTemplate.exchange("https://REMOTE_URI/sameObjects", GET, entity, MyList.class);
I tried header with differents values (User-Agent, Host, ..) and ResponseEntity<Object> , but I have always the same error :
org.springframework.web.client.HttpClientErrorException$Forbidden: 403 Forbidden: [no body]
I can access it with Postman with the p12 certificate.
Thank you for your help
I found the solution, I change RestTemplate :
public RestTemplate getRestTemplate() {
try {
final KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(keyStoreFile.getInputStream(), keyPassword.toCharArray());
final SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(keyStoreFile.getURL(), keyPassword.toCharArray(), (X509Certificate[] chain, String authType) -> true)
.loadKeyMaterial(keyStore, keyPassword.toCharArray())
.build();
final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext, INSTANCE);
final HttpClient httpClient = custom()
.setSSLSocketFactory(socketFactory)
.build();
final HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
final RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(requestFactory));
return restTemplate;
} catch (IOException e) {
log.error("....", e);
throw new ApiException(e);
} catch (Exception e) {
log.error("....", e);
throw new ApiException(e);
}
}

Spring Boot Rest Template Keep Connection Alive

I have a Spring Boot application that is creating a request to an external system. The external system is responding after some time, 3-4 minutes. I would like to keep the connection open until i receive an response from the remote API. I tried using webflux, i tried setup the connection timeout for my application in application.yml file. I could make the application to wait for a response more than 2 minutes. Some code that i have tried
#Configuration
public class RestConfigurations {
#Bean(name = "restTemplate")
public RestTemplate getRestTemplate(RestTemplateBuilder restTemplateBuilder) {
RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
// Error handler
restTemplate.setErrorHandler(new CustomRestTemplateErrorHandler());
// Media converters
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(Arrays.asList(
MediaType.APPLICATION_JSON,
MediaType.TEXT_PLAIN,
MediaType.TEXT_HTML));
restTemplate.getMessageConverters().add(converter);
return restTemplate;
}
private ClientHttpRequestFactory getClientHttpRequestFactory() {
int connectionTimeout = 15*60000; // milliseconds
int socketTimeout = 15*60000; // milliseconds
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(connectionTimeout)
.setConnectionRequestTimeout(connectionTimeout)
.setSocketTimeout(socketTimeout)
.build();
CloseableHttpClient client = HttpClientBuilder
.create()
.setDefaultRequestConfig(config)
.setKeepAliveStrategy(connectionKeepAliveStrategy())
.build();
return new HttpComponentsClientHttpRequestFactory(client);
}
public HttpComponentsClientHttpRequestFactory getHttpClientFactory() {
HttpComponentsClientHttpRequestFactory httpClientFactory = new
HttpComponentsClientHttpRequestFactory(
HttpClients.createDefault()
);
httpClientFactory.setConnectTimeout(15 * 600000);
httpClientFactory.setReadTimeout(15 * 600000);
httpClientFactory.setConnectionRequestTimeout(15 * 600000);
return httpClientFactory;
}
public ClientHttpRequestFactory createRequestFactory(){
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(400);
connectionManager.setDefaultMaxPerRoute(200);
RequestConfig requestConfig = RequestConfig
.custom()
.setConnectionRequestTimeout(5000)
.setSocketTimeout(10000)
.build();
CloseableHttpClient httpClient = HttpClients
.custom()
.setConnectionManager(connectionManager)
.setKeepAliveStrategy(connectionKeepAliveStrategy())
.setDefaultRequestConfig(requestConfig)
.build();
return new HttpComponentsClientHttpRequestFactory(httpClient);
}
private ConnectionKeepAliveStrategy connectionKeepAliveStrategy() {
return (response,context)-> {
HeaderElementIterator it = new BasicHeaderElementIterator(
response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if ( value != null && param.equalsIgnoreCase("timeout")){
try {
return Long.parseLong(value) * 999999999;
} catch (NumberFormatException exception) {
exception.printStackTrace();
}
}
}
return 15 * 60000;
} ;
}
#Bean(name = "webClient")
public WebClient webClient() {
TcpClient tcpClient = TcpClient
.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 60000 * 15)//15 minutes
.doOnConnected(connection -> {
connection.addHandlerLast(new ReadTimeoutHandler(15, TimeUnit.MINUTES));
connection.addHandlerLast(new WriteTimeoutHandler(15, TimeUnit.MINUTES));
});
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient).wiretap(true)))
.build();
}
None of this configs worked. I tried using okhttpclient like this.
public class OkHttpClientFactoryImpl implements OkHttpClientFactory {
#Override
public OkHttpClient.Builder createBuilder(boolean disableSslValidation) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
ConnectionPool okHttpConnectionPool = new ConnectionPool(50, 30, TimeUnit.SECONDS);
builder.connectionPool(okHttpConnectionPool);
builder.connectTimeout(20, TimeUnit.MINUTES);
builder.readTimeout(20, TimeUnit.MINUTES);
builder.writeTimeout(20, TimeUnit.MINUTES);
builder.retryOnConnectionFailure(false);
return builder;
}
}
#Bean
#Qualifier("OKSpringCommonsRestTemplate")
public ClientHttpRequestFactory createOKCommonsRequestFactory() {
OkHttpClientFactoryImpl httpClientFactory = new OkHttpClientFactoryImpl();
OkHttpClient client = httpClientFactory.createBuilder(false).build();
return new OkHttp3ClientHttpRequestFactory(client);
}
It did not work. I don't know what could cause the connection to close other than the things i've setup.
After receiving an answer, i tried :
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofMinutes(10))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpClient client= HttpClient.newHttpClient();
try {
client.send(request, HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
The result is the same. After 2 minutes i get an error saying java.io.IOException: HTTP/1.1 header parser received no bytes
I found the problem. It was in fact because of the server on the other end of the call. Express server is closing the connection, by default, after 2 minutes.
Increase Client Idle Timeout at server/LB to a higher number.

Resources