Spring WS - HTTPS Client-Server Example

9 minute read

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.

If you want to learn more about Spring WS - head on over to the Spring WS tutorials page.

General Project Setup

Tools used:

  • Spring-WS 2.4
  • HttpClient 4.5
  • Spring Boot 1.5
  • Maven 3.5

The setup of the project is based on a previous Spring WS example but the basic helloworld.wsdl has been replaced by a more generic ticketagent.wsdl from the W3C WSDL 1.1 specification.

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.

We will use the HttpComponentsMessageSender implementation in below example as it contains more advanced and easy-to-use functionality. On GitHub, however, we have also added a HTTPS example that uses the HttpsUrlConnectionMessageSender implementation in case a dependency on the HttpClient is not desired.

In order to use the HttpComponentsMessageSender implementation, we need to add the Apache httpclient dependency to the Maven POM file.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.codenotfound</groupId>
  <artifactId>spring-ws-https-httpclient</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>spring-ws-https-httpclient</name>
  <description>Spring WS - HTTPS Client Server Example</description>
  <url>https://www.codenotfound.com/spring-ws-https-client-server-example.html</url>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.9.RELEASE</version>
  </parent>

  <properties>
    <java.version>1.8</java.version>
    <httpclient.version>4.5.4</httpclient.version>
    <maven-jaxb2-plugin.version>0.13.3</maven-jaxb2-plugin.version>
  </properties>

  <dependencies>
    <!-- spring-boot -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web-services</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- httpclient -->
    <dependency>
      <groupId>org.apache.httpcomponents</groupId>
      <artifactId>httpclient</artifactId>
      <version>${httpclient.version}</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <!-- spring-boot-maven-plugin -->
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <!-- maven-jaxb2-plugin -->
      <plugin>
        <groupId>org.jvnet.jaxb2.maven2</groupId>
        <artifactId>maven-jaxb2-plugin</artifactId>
        <version>${maven-jaxb2-plugin.version}</version>
        <executions>
          <execution>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <schemaDirectory>${project.basedir}/src/main/resources/wsdl</schemaDirectory>
          <schemaIncludes>
            <include>*.wsdl</include>
          </schemaIncludes>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

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'.

keytool -genkeypair -alias server-keypair -keyalg RSA -keysize 2048 -validity 3650 -dname "CN=server,O=codenotfound.com" -keypass server-key-p455w0rd -keystore server-keystore.jks -storepass server-keystore-p455w0rd

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.

server keystore

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.

keytool -exportcert -alias server-keypair -file server-public-key.cer -keystore server-keystore.jks -storepass server-keystore-p455w0rd

If you want you can use the Examine Certificate menu in Portecle to visualize the certificate.

server public key

Now we create a client-truststore.jks that contains the exported certificate by executing following keytool command.

keytool -importcert -keystore client-truststore.jks -alias server-public-key -file server-public-key.cer -storepass client-truststore-p455w0rd -noprompt

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

client truststore

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.

https jks files

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.

client:
  default-uri: https://localhost:9443/codenotfound/ws/ticketagent
  ssl:
    trust-store: classpath:jks/client-truststore.jks
    trust-store-password: client-truststore-p455w0rd

In the ClientConfig class we need to enable the WebServiceTemplate to connect using the HTTPS protocol. This is done by creating and setting a HttpComponentsMessageSender on which we then configure the HttpClient which provides full support for HTTP over Secure Sockets Layer (SSL) or Transport Layer Security (TLS) protocols.

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.

In order to fix this problem, we could regenerate the server keypair so it contains 'localhost'. You can find the needed keytool command in the Spring WS mutual authentication tutorial.

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.

package com.codenotfound.ws.client;

import javax.net.ssl.SSLContext;

import org.apache.http.client.HttpClient;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.ssl.SSLContextBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.ws.client.core.WebServiceTemplate;
import org.springframework.ws.transport.http.HttpComponentsMessageSender;
import org.springframework.ws.transport.http.HttpComponentsMessageSender.RemoveSoapHeadersInterceptor;

@Configuration
public class ClientConfig {

  @Value("${client.default-uri}")
  private String defaultUri;

  @Value("${client.ssl.trust-store}")
  private Resource trustStore;

  @Value("${client.ssl.trust-store-password}")
  private String trustStorePassword;

  @Bean
  Jaxb2Marshaller jaxb2Marshaller() {
    Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
    jaxb2Marshaller.setContextPath("org.example.ticketagent");

    return jaxb2Marshaller;
  }

  @Bean
  public WebServiceTemplate webServiceTemplate() throws Exception {
    WebServiceTemplate webServiceTemplate = new WebServiceTemplate();
    webServiceTemplate.setMarshaller(jaxb2Marshaller());
    webServiceTemplate.setUnmarshaller(jaxb2Marshaller());
    webServiceTemplate.setDefaultUri(defaultUri);
    webServiceTemplate.setMessageSender(httpComponentsMessageSender());

    return webServiceTemplate;
  }

  @Bean
  public HttpComponentsMessageSender httpComponentsMessageSender() throws Exception {
    HttpComponentsMessageSender httpComponentsMessageSender = new HttpComponentsMessageSender();
    httpComponentsMessageSender.setHttpClient(httpClient());

    return httpComponentsMessageSender;
  }

  public HttpClient httpClient() throws Exception {
    return HttpClientBuilder.create().setSSLSocketFactory(sslConnectionSocketFactory())
        .addInterceptorFirst(new RemoveSoapHeadersInterceptor()).build();
  }

  public SSLConnectionSocketFactory sslConnectionSocketFactory() throws Exception {
    // NoopHostnameVerifier essentially turns hostname verification off as otherwise following error
    // is thrown: java.security.cert.CertificateException: No name matching localhost found
    return new SSLConnectionSocketFactory(sslContext(), NoopHostnameVerifier.INSTANCE);
  }

  public SSLContext sslContext() throws Exception {
    return SSLContextBuilder.create()
        .loadTrustMaterial(trustStore.getFile(), trustStorePassword.toCharArray()).build();
  }
}

Setup HTTPS on the Server

As our web service runs on Spring Boot, we just need to configure the underlying web server with the correct parameters. This is done via the Spring Boot web properties (look for the # EMBEDDED SERVER CONFIGURATION heading).

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.

server:
  port: 9443
  ssl:
    key-store: classpath:jks/server-keystore.jks
    key-store-password: server-keystore-p455w0rd
    key-alias: server-keypair
    key-password: server-key-p455w0rd

In order to quickly test if the setup was successful, start Spring Boot by running below Maven command at the command prompt.

mvn spring-boot:run

Open below URL in your browser and the ticket agent service WSDL definition will now be served over HTTPS: https://localhost:9443/codenotfound/ws/ticketagent.wsdl

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.

https ticketagent wsdl

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.

mvn test

This will result in a successful test run as shown below.

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.9.RELEASE)

08:42:52.929 [main] INFO  c.c.ws.SpringWsApplicationTests - Starting SpringWsApplicationTests on cnf-pc with PID 5352 (started by CodeNotFound in c:\code\spring-ws\spring-ws-https)
08:42:52.932 [main] INFO  c.c.ws.SpringWsApplicationTests - No active profile set, falling back to default profiles: default
08:42:55.727 [main] INFO  c.c.ws.SpringWsApplicationTests - Started SpringWsApplicationTests in 3.096 seconds (JVM running for 3.798)
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.484 sec - in com.codenotfound.ws.SpringWsApplicationTests

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6.275 s
[INFO] Finished at: 2017-05-01T08:42:56+02:00
[INFO] Final Memory: 20M/227M
[INFO] ------------------------------------------------------------------------

If you would like more information when debugging SL/TLS connections add "-Djavax.net.debug=ssl,handshake" at the end of your Maven command as shown below.

mvn test -Djavax.net.debug=ssl,handshake

github mark If you would like to run the above code sample you can get the full source code here.

Although setting up HTTPS using Spring WS is not extensively covered in the reference documentation, it can be done quite easily using configuration and some builder classes.

If you found this tutorial helpful or if you run into some problems let me know in the comments section below.

Leave a comment