53

I'm trying to find examples of WebClient use.

My goal is to use Spring 5 WebClient to query a REST service using https and self signed certificate

Any example?

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
Seb
  • 3,602
  • 8
  • 36
  • 52

6 Answers6

57

Looks like Spring 5.1.1 (Spring boot 2.1.0) removed HttpClientOptions from ReactorClientHttpConnector, so you can not configure options while creating instance of ReactorClientHttpConnector

One option that works now is:

val sslContext = SslContextBuilder
            .forClient()
            .trustManager(InsecureTrustManagerFactory.INSTANCE)
            .build()
val httpClient = HttpClient.create().secure { t -> t.sslContext(sslContext) }
val webClient = WebClient.builder().clientConnector(ReactorClientHttpConnector(httpClient)).build()

Basically while creating the HttpClient, we are configuring the insecure sslContext, and then passing this httpClient for use in ReactorClientHttpConnector globally.

The other option is to configure TcpClient with insecure sslContext and use it to create HttpClient instance, as illustrated below:

val sslContext = SslContextBuilder
            .forClient()
            .trustManager(InsecureTrustManagerFactory.INSTANCE)
            .build()
val tcpClient = TcpClient.create().secure { sslProviderBuilder -> sslProviderBuilder.sslContext(sslContext) }
val httpClient = HttpClient.from(tcpClient)
val webClient =  WebClient.builder().clientConnector(ReactorClientHttpConnector(httpClient)).build()

For more information:

Update: Java version of the same code

SslContext context = SslContextBuilder.forClient()
    .trustManager(InsecureTrustManagerFactory.INSTANCE)
    .build();
                
HttpClient httpClient = HttpClient.create().secure(t -> t.sslContext(context));

WebClient wc = WebClient
                    .builder()
                    .clientConnector(new ReactorClientHttpConnector(httpClient)).build();
Adil Khalil
  • 2,073
  • 3
  • 21
  • 33
Munish Chandel
  • 3,572
  • 3
  • 24
  • 35
  • 1
    "_The other option is to configure TcpClient with insecure sslContext and use it to create HttpClient instance_" seems to be broken with the release of Spring Boot 2.4.0 (reactor-netty 1.0.1). You need to either add `httpClient = httpClient.secure(tcpClient.configuration().sslProvider());` to carry on using `TcpClient` or use `HttpClientConfig` as a workaround until the fix for [reactor-netty#1382](https://github.com/reactor/reactor-netty/issues/1382) gets released. – Mert Z. Nov 16 '20 at 00:10
51

See example of use insecure TrustManagerFactory that trusts all X.509 certificates (including self-signed) without any verification. The important note from documentation:

Never use this TrustManagerFactory in production. It is purely for testing purposes, and thus it is very insecure.

@Bean
public WebClient createWebClient() throws SSLException {
    SslContext sslContext = SslContextBuilder
            .forClient()
            .trustManager(InsecureTrustManagerFactory.INSTANCE)
            .build();
    ClientHttpConnector httpConnector = HttpClient.create().secure(t -> t.sslContext(sslContext) )
    return WebClient.builder().clientConnector(httpConnector).build();
}
Wes
  • 6,697
  • 6
  • 34
  • 59
Venelin
  • 619
  • 9
  • 12
  • thanks for your answer, I also need to set timeout on read and connection, how can I achieve that ? – Seb Aug 17 '17 at 09:59
  • 1
    Have one comment though. The last line should be WebClient.builder().clientConnector(httpConnector).build(); otherwise won't compile. – Danylo Zatorsky Jul 22 '18 at 21:17
  • 10
    This solution stopped working after upgrading Spring Boot to 2.1.0 which brings in Spring 5.1.1, https://stackoverflow.com/a/53147631/2172731 this worked for me using Spring Security 5.1.1. – Munish Chandel Nov 06 '18 at 14:30
17

Had to edit this, to accommodate spring-boot 2.0->2.1 changes.

Another way, if you want to program production code is, to create a spring bean like such, that modifies the injected WebClient, using the settings from the spring-boot server for where the truststore and Keystore are. In the client, you only need to give the Keystore, if you are using 2-way-ssl. Not sure, why the ssl-stuff is not preconfigured and easily injectable, similar to the really cool spring-boot server settings.

import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
.
.
.

  @Bean
  WebClientCustomizer configureWebclient(@Value("${server.ssl.trust-store}") String trustStorePath, @Value("${server.ssl.trust-store-password}") String trustStorePass,
      @Value("${server.ssl.key-store}") String keyStorePath, @Value("${server.ssl.key-store-password}") String keyStorePass, @Value("${server.ssl.key-alias}") String keyAlias) {

      return (WebClient.Builder webClientBuilder) -> {
          SslContext sslContext;
          final PrivateKey privateKey;
          final X509Certificate[] certificates;
          try {
            final KeyStore trustStore;
            final KeyStore keyStore;
            trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
            trustStore.load(new FileInputStream(ResourceUtils.getFile(trustStorePath)), trustStorePass.toCharArray());
            keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(new FileInputStream(ResourceUtils.getFile(keyStorePath)), keyStorePass.toCharArray());
            List<Certificate> certificateList = Collections.list(trustStore.aliases())
                .stream()
                .filter(t -> {
                  try {
                    return trustStore.isCertificateEntry(t);
                  } catch (KeyStoreException e1) {
                    throw new RuntimeException("Error reading truststore", e1);
                  }
                })
                .map(t -> {
                  try {
                    return trustStore.getCertificate(t);
                  } catch (KeyStoreException e2) {
                    throw new RuntimeException("Error reading truststore", e2);
                  }
                })
                .collect(Collectors.toList());
            certificates = certificateList.toArray(new X509Certificate[certificateList.size()]);
            privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyStorePass.toCharArray());
            Certificate[] certChain = keyStore.getCertificateChain(keyAlias);
            X509Certificate[] x509CertificateChain = Arrays.stream(certChain)
                .map(certificate -> (X509Certificate) certificate)
                .collect(Collectors.toList())
                .toArray(new X509Certificate[certChain.length]);
            sslContext = SslContextBuilder.forClient()
                .keyManager(privateKey, keyStorePass, x509CertificateChain)
                .trustManager(certificates)
                .build();
  
            HttpClient httpClient = HttpClient.create()
                .secure(sslContextSpec -> sslContextSpec.sslContext(sslContext));
            ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
            webClientBuilder.clientConnector(connector);
          } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException | UnrecoverableKeyException e) {
            throw new RuntimeException(e);
          }
        };
  }

Here the part, where you use the Webclient:

import org.springframework.web.reactive.function.client.WebClient;

@Component
public class ClientComponent {

  public ClientComponent(WebClient.Builder webClientBuilder, @Value("${url}") String url) {
    this.client = webClientBuilder.baseUrl(solrUrl).build();
  }
}
Saikat
  • 14,222
  • 20
  • 104
  • 125
Frischling
  • 2,100
  • 14
  • 34
  • When the WebClientBuilder is injected into the component, it will automatically be customized with the WebClientCustomizer bean? – Skywarp Nov 15 '18 at 18:34
  • Yes, that's it. If I remember correctly, I could inject the WebClient directly in the nomral application, but had to get it from the builder for unit-tests. I didn't figure out, why this was, though, you might have to try - please feel free to correct this here, if you can get the WebClient directly. – Frischling Nov 16 '18 at 10:24
  • 1
    I cannot get this to work. I tried it both through injection and by explicitly creating an SslContext, which I passed as an option to a ReactorClientHttpConnector, which in turn I passed into the builder to build the WebClient. But the server I'm calling says I am not presenting a certificate. I double checked the keystores and truststores both client and server side and they are valid. Also, the server is accessible via RestTemplate configured for 2-way TLS as well as SOAP UI. – Skywarp Nov 16 '18 at 17:27
  • 1
    I fixed it but not sure why yours didn't work. I changed `.keyManager((PrivateKey) keyStore.getKey(keyAlias, keyStorePass.toCharArray()))` to `.keyManager(keyManagerFactory)` after initializing a `KeyManagerFactory` with the keystore, and the server finally accepted the certificate. – Skywarp Nov 16 '18 at 21:34
  • 2
    I could test my old code, and the problem was, that it's apparently a difference between private key's certificateChain, and the trustchain, even if it contains the same keys. The code I now posted is tested and worked, but ordering of the keys in the certificateChain can break the trustchain (at least I saw this happen). Hopefully this is more helpful to others now. I don't know why this standard usecase is so complicated. – Frischling Mar 13 '19 at 11:23
4

This one work for me. Spring framework version 5.3.23 (Spring boot version 2.7.4) You can try this way using ReactorClientHttpConnector:

SslContext context = SslContextBuilder.forClient()
    .trustManager(InsecureTrustManagerFactory.INSTANCE)
    .build();
                
HttpClient httpClient = HttpClient.create().secure(t -> t.sslContext(context));

WebClient wc = WebClient
                    .builder()
                    .clientConnector(new ReactorClientHttpConnector(httpClient)).build();

hope this answer is helpful for you.

Praveen Kumar Verma
  • 2,988
  • 2
  • 18
  • 31
2

For someone who might have stuck on how to consume a https protected REST API with reactive WebFlux webClient

You want to create two things

  1. https enabled REST API - https://github.com/mghutke/HttpsEnabled
  2. Another REST API as a client with WebClient to consume above one - https://github.com/mghutke/HttpsClient

Note: please go through above project look at keystore which is shared with Both the above spring boot apps. And programmatically added the keyManagerFactory and TrustManagerFactory.

MonishGhutke
  • 109
  • 1
  • 6
0

Also, if we need to configure multiple SSLContexts, for example, we have REST API 1 and REST API 2 and for them we configured SSLContext sslContext1 and SSLContext sslContext2

The thing is that HttpClient.create().secure(...) allows us to apply only one SSLContext, but in our case we want multiple.

So, the solution in our case would be to create two different WebClient with different ReactorClientHttpConnector

// for REST API 1 with sslContext1
WebClient webClient1 = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(
                HttpClient.create()
                        .secure(sslContextSpec -> sslContextSpec.sslContext(sslContext1))
        ))
        .build();

// for REST API 1 with sslContext2
WebClient webClient2 = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(
                HttpClient.create()
                        .secure(sslContextSpec -> sslContextSpec.sslContext(sslContext2))
        ))
        .build();

And we're done!

Also, just to point out, these clients will share the event loop group by default, which is recommended. However, they won't, if you configure them using runOn or with using ReactorResourceFactory, more about resources can be found here: https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-client-builder-reactor-resources

kerbermeister
  • 2,985
  • 3
  • 11
  • 30