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
  • Spring Security 4.2
  • 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.

Security related features of Spring-WS are not part of the spring-boot-starter-web-services Spring Boot starter. As such we have to add two extra dependencies to the Maven POM file in order for the example to work.

First, the spring-ws-security dependency which contains a FactoryBean for setting up the client’s TrustStore. Second, the spring-ws-support dependency which contains a MessageSender that adds support for (self-signed) HTTPS certificates.

<?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</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

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

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

  <properties>
    <java.version>1.8</java.version>

    <maven-jaxb2-plugin.version>0.13.2</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>
    <!-- spring-ws -->
    <dependency>
      <groupId>org.springframework.ws</groupId>
      <artifactId>spring-ws-security</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.ws</groupId>
      <artifactId>spring-ws-support</artifactId>
    </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 our application.yml configuration 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 HttpsUrlConnectionMessageSender which is an extension of the default HttpUrlConnectionMessageSender with support for HTTPS.

Note that the HttpsUrlConnectionMessageSender is part of the spring-ws-support package.

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.

To easily load one or more truststores using Spring configuration, we can use the KeyStoreFactoryBean that ships with the spring-ws-security dependency that was added to the project’s pom.xml. The bean has a resource location property and password, which both need to be set.

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 override the HostnameVerifier so that it returns true in the case a hostname equals to 'localhost' is used. Note that this is not something you would want to do in production!

package com.codenotfound.ws.client;

import javax.net.ssl.HostnameVerifier;

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.soap.security.support.KeyStoreFactoryBean;
import org.springframework.ws.soap.security.support.TrustManagersFactoryBean;
import org.springframework.ws.transport.http.HttpsUrlConnectionMessageSender;

@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);
    // set a httpsUrlConnectionMessageSender to handle the HTTPS session
    webServiceTemplate.setMessageSender(httpsUrlConnectionMessageSender());

    return webServiceTemplate;
  }

  @Bean
  public HttpsUrlConnectionMessageSender httpsUrlConnectionMessageSender() throws Exception {
    HttpsUrlConnectionMessageSender httpsUrlConnectionMessageSender =
        new HttpsUrlConnectionMessageSender();
    httpsUrlConnectionMessageSender.setTrustManagers(trustManagersFactoryBean().getObject());
    // allows the client to skip host name verification as otherwise following error is thrown:
    // java.security.cert.CertificateException: No name matching localhost found
    httpsUrlConnectionMessageSender.setHostnameVerifier(new HostnameVerifier() {
      @Override
      public boolean verify(String hostname, javax.net.ssl.SSLSession sslSession) {
        if ("localhost".equals(hostname)) {
          return true;
        }
        return false;
      }
    });

    return httpsUrlConnectionMessageSender;
  }

  @Bean
  public KeyStoreFactoryBean trustStore() {
    KeyStoreFactoryBean keyStoreFactoryBean = new KeyStoreFactoryBean();
    keyStoreFactoryBean.setLocation(trustStore);
    keyStoreFactoryBean.setPassword(trustStorePassword);

    return keyStoreFactoryBean;
  }

  @Bean
  public TrustManagersFactoryBean trustManagersFactoryBean() {
    TrustManagersFactoryBean trustManagersFactoryBean = new TrustManagersFactoryBean();
    trustManagersFactoryBean.setKeyStore(trustStore().getObject());

    return trustManagersFactoryBean;
  }
}

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.

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.4.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] ------------------------------------------------------------------------

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 support 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