Spring WS - HTTPS Client-Server Example

HTTPS is a protocol for secure communication over a computer network. It consists of communication over Hypertext Transfer Protocol (HTTP) within a connection encrypted by Transport Layer Security (TLS), or its predecessor, Secure Sockets Layer (SSL).

A web service exposed on HTTPS provides authentication of the associated web server with which one is communicating. In addition, it provides bidirectional encryption of communications between the client and server, that protects against eavesdropping and tampering with the contents of the communication.

The following example shows how to configure both client and server in order to consume and respectively expose a web service over HTTPS using Spring-WS, Spring Boot, and Maven.

There are two implementations of the WebServiceMessageSender interface for sending messages via HTTPS. The default implementation is the HttpsUrlConnectionMessageSender, which uses the facilities provided by Java itself. The alternative is the HttpComponentsMessageSender, which uses the Apache HttpComponents HttpClient.

Since applications can communicate either with or without TLS (or SSL), it is necessary for the client to indicate to the server the setup of a TLS connection. One of the main ways of achieving this is to use a different port number for TLS connections. In this example we will use port 9443 instead of port 9090.

Once the client and server have agreed to use TLS, they negotiate a stateful connection by using a handshaking procedure. During this procedure, the server usually sends back its identification in the form of a digital certificate.

Java programs store certificates in a repository called Java KeyStore (JKS). To generate the keystore and certificate for this example we use keytool which is a key and certificate management utility that ships with Java.

Open a command prompt at the root of your Maven project and execute following statement to generate a public/private keypair for the server side. The result will be a server-keystore.jks Java Keystore file that contains a key pair called 'server-keypair'.

If you would like to visualize the content of the keystore you can use a tool like Portecle. Using the File menu, navigate to the server-keystore.jks JKS file and when prompted enter the keystore password (in the above command we used "server-keystore-p455w0rd") and the result should be should be similar to what is shown below.

For the client, we need to create a truststore (also a JKS file) which contains certificates from other parties that you expect to communicate with, or from Certificate Authorities (CA) that you trust to identify other parties. In this example, we will add the server’s public certificate to the client’s truststore. As a result, our client will “trust” and thus allow an HTTPS connection to the server.

To create the truststore we first need to export the public key certificate or digital certificate of the server. Use following command to generate a server-public-key.cer certificate file.

Similar to the keystore we can open the truststore using Portecle to inspect its contents.

Finally, we move the three artifacts we have just generated: client-truststore.jks, server-keystore.jks and server-public-key.cer to the src/main/resources folder so that they are available on the classpath for both client and server setup.

Setup HTTPS on the Client

As the server will expose the Ticket Agent service on HTTPS we need to change the default URI (service address) that is set on the WebServiceTemplate used by the client. The @Value annotation is used to inject the 'client.default-uri' value from the application properties YAML file.

There are two other values that are configured in the application.yml properties file. These are are the location of the truststore JKS file and its password as shown below.

HttpClient makes use of SSLConnectionSocketFactory to create SSL connections. SSLConnectionSocketFactory allows for a high degree of customization. It can take an instance of SSLContext as a parameter and use it to create custom configured TLS/SSL connections.

During the TLS handshaking procedure, the client needs to decide whether it trusts the public key certificate that the server provides. This is done based on whether or not this certificate (or one of its issuing CA’s) is present in (one of) the client’s truststores. We specify a TrustManagersFactoryBean to handle the configured truststores.

In order to trust the server certificate, create an sslContext() bean on which we load the truststore using the JKS file and its corresponding password. This context is then passed to the sslConnectionSocketFactory() bean which is in turn set on the httpClient().

If we were to test the client with above settings we would run into the following exception

javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No name matching localhost found

The reason for this is that when the HTTPS client connects to a server, it’s not enough for a certificate to be trusted, it also has to match the server you want to talk to. In other words, the client verifies that the hostname in the certificate matches the hostname of the server. For more detailed information check this answer on Stack Overflow.

Another option, which we will use in this example, is to turn hostname verification off. Apache ships a NoopHostnameVerifier that can be used for this. Simply pass an instance to the SSLConnectionSocketFactory constructor. Note that this is not something you would want to do in production!

There is one last problem we need to take care of. The HttpComponentsMessageSender has two constructors, with and without HttpClient, and the one with HttpClient omits adding a SoapRemoveHeaderInterceptor. The HttpClient throws an exception if Content-Length or Transfer-Encoding headers have been set.

So in order to make sure those headers are not present we add the SoapRemoveHeaderInterceptor by using the addInterceptorFirst() method on the HttpClientBuilder.

packagecom.codenotfound.ws.client;importjavax.net.ssl.SSLContext;importorg.apache.http.client.HttpClient;importorg.apache.http.conn.ssl.NoopHostnameVerifier;importorg.apache.http.conn.ssl.SSLConnectionSocketFactory;importorg.apache.http.impl.client.HttpClientBuilder;importorg.apache.http.ssl.SSLContextBuilder;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.core.io.Resource;importorg.springframework.oxm.jaxb.Jaxb2Marshaller;importorg.springframework.ws.client.core.WebServiceTemplate;importorg.springframework.ws.transport.http.HttpComponentsMessageSender;importorg.springframework.ws.transport.http.HttpComponentsMessageSender.RemoveSoapHeadersInterceptor;@ConfigurationpublicclassClientConfig{@Value("${client.default-uri}")privateStringdefaultUri;@Value("${client.ssl.trust-store}")privateResourcetrustStore;@Value("${client.ssl.trust-store-password}")privateStringtrustStorePassword;@BeanJaxb2Marshallerjaxb2Marshaller(){Jaxb2Marshallerjaxb2Marshaller=newJaxb2Marshaller();jaxb2Marshaller.setContextPath("org.example.ticketagent");returnjaxb2Marshaller;}@BeanpublicWebServiceTemplatewebServiceTemplate()throwsException{WebServiceTemplatewebServiceTemplate=newWebServiceTemplate();webServiceTemplate.setMarshaller(jaxb2Marshaller());webServiceTemplate.setUnmarshaller(jaxb2Marshaller());webServiceTemplate.setDefaultUri(defaultUri);webServiceTemplate.setMessageSender(httpComponentsMessageSender());returnwebServiceTemplate;}@BeanpublicHttpComponentsMessageSenderhttpComponentsMessageSender()throwsException{HttpComponentsMessageSenderhttpComponentsMessageSender=newHttpComponentsMessageSender();httpComponentsMessageSender.setHttpClient(httpClient());returnhttpComponentsMessageSender;}publicHttpClienthttpClient()throwsException{returnHttpClientBuilder.create().setSSLSocketFactory(sslConnectionSocketFactory()).addInterceptorFirst(newRemoveSoapHeadersInterceptor()).build();}publicSSLConnectionSocketFactorysslConnectionSocketFactory()throwsException{// NoopHostnameVerifier essentially turns hostname verification off as otherwise following error// is thrown: java.security.cert.CertificateException: No name matching localhost foundreturnnewSSLConnectionSocketFactory(sslContext(),NoopHostnameVerifier.INSTANCE);}publicSSLContextsslContext()throwsException{returnSSLContextBuilder.create().loadTrustMaterial(trustStore.getFile(),trustStorePassword.toCharArray()).build();}}

In this example, we use the YAML format to specify the different parameters in the application properties file as shown below. The server HTTP port is set to '9443' in order to indicate the usage of HTTPS. The server’s keystore (that was generated at the beginning of this tutorial) and the corresponding password are also configured in addition to the alias of the key pair to be used and the corresponding password.

Notice that your browser will probably flag the connection as being not secure (go ahead and accept an exception). The reason for this is that we are using self-signed certificates which are by default untrusted by your browser.

Testing Spring WS over HTTPS

In order to test the example, we can trigger the existing SpringWsApplicationTests unit test case by running following Maven command.