How to implement security in SOAP web service using Spring-WS

Overview

In this tutorial, we'll see how to implement security in SOAP web service. Normally we use two types of security in SOAP web service.

1) WS-Security using policies 2) Basic Authentication

For this tutorial, we'll implement the policy-based approach and all the configuration will be annotation-based.

Pre-requisties

  1. JDK 1.8 +
  2. Maven
  3. IDE

Approach

SOAP services can be developed with two methods

  1. Contract First: Define WSDL and Schema before writing any code.
  2. Contract Last: Auto-generate the WSDL and schemas from the java classes.

Spring-WS only supports the contract-first approach

Project setup

You can clone this project from Github to kick start the project

Create a maven project and add the following dependencies in the pom.xml

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.1.3.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web-services</artifactId>
    </dependency>
    <dependency>
        <groupId>wsdl4j</groupId>
        <artifactId>wsdl4j</artifactId>
    </dependency>
</dependencies>

Schema Design

The contract-first approach requires us to define the schema. And then we'll use Spring-WS auto-generate WSDL out of the schema.

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema
    xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://spring.tutorialflix.com/types/v1"
    xmlns:tns="http://spring.tutorialflix.com/types/v1" elementFormDefault="qualified">
    <xs:element name="createCustomerRequest">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="customerName">
                    <xs:simpleType>
                        <xs:restriction base="xs:string">
                            <xs:maxLength value="50" />
                            <xs:whiteSpace value="collapse" />
                        </xs:restriction>
                    </xs:simpleType>
                </xs:element>
                <xs:element name="customerAge">
                    <xs:simpleType>
                        <xs:restriction base="xs:integer" />
                    </xs:simpleType>
                </xs:element>
                <xs:element name="customerCity">
                    <xs:simpleType>
                        <xs:restriction base="xs:string">
                            <xs:maxLength value="50" />
                            <xs:whiteSpace value="collapse" />
                        </xs:restriction>
                    </xs:simpleType>
                </xs:element>
                <xs:element name="customerPhoneNumber">
                    <xs:simpleType>
                        <xs:restriction base="xs:string">
                            <xs:maxLength value="10" />
                            <xs:whiteSpace value="collapse" />
                        </xs:restriction>
                    </xs:simpleType>
                </xs:element>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:element name="createCustomerResponse">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="customerID" type="xs:integer" />
                <xs:element name="details" type="xs:string" />
                <xs:element name="status" type="xs:string" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:element name="createCustomerFault">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="errorMessage" type="xs:normalizedString" />
                <xs:element name="errorCode" type="xs:int" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>

Generate Java Classes

Now, we'll jaxb2-maven-plugin to generate the java classes from the schema. Add the below plugin in your pom.xml

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>jaxb2-maven-plugin</artifactId>
    <version>1.6</version>
    <executions>
        <execution>
            <id>xjc</id>
            <goals>
                <goal>xjc</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <schemaDirectory>${project.basedir}/src/main/resources/</schemaDirectory>
        <outputDirectory>${project.basedir}/src/main/java</outputDirectory>
        <clearOutputDir>false</clearOutputDir>
    </configuration>
</plugin>
  • schemaDirectory : location of the schema
  • outputDirectory: where we want our java classes.
  • clearOutputDir : making this true will delete the classes every time you compile the project

Now, we'll generate the classes by issuing the following maven command.

$ mvn clean install

Now you can see the auto-generated classes in your project folder.

Setup Endpoint

Now, we'll set up an endpoint in our Java code to serve the request. Create a class and annotate with @Endpoint

package com.tutorialflix.spring.ws.endpoint;

import java.math.BigInteger;

import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;

import com.tutorialflix.spring.types.v1.CreateCustomerRequest;
import com.tutorialflix.spring.types.v1.CreateCustomerResponse;

@Endpoint
public class CustomerServiceEndpoint {

 @ResponsePayload
 @PayloadRoot(localPart = "createCustomerRequest", namespace = "http://spring.tutorialflix.com/types/v1")
 public CreateCustomerResponse createCustomer(@RequestPayload CreateCustomerRequest request) {

  CreateCustomerResponse response = new CreateCustomerResponse();
  response.setCustomerID(BigInteger.ONE);
  response.setDetails(request.getCustomerName() + " " + request.getCustomerCity() + " " + request.getCustomerPhoneNumber());
  response.setStatus("SUCCESS");
  return response;
 }

}

For the purpose of this tutorial, I added a very simple code to return a successful response.

  • @Endpoint: This indicates that this class is a web service endpoint
  • @PayloadRoot: This indicates that incoming soap requests for this method will have a defined local part and namespace. It will basically try to match the RootElement of your XML message.
  • @ResponsePayload: This indicates that the method will return a payload.

Configure Servlet Bean & WSDL Definition

  • Define the ServletRegistrationBean in configuration to register a servlet that will listen to the incoming requests.
  • Define the configuration for WSDL Definition
package com.tutorialflix.spring.config;

import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ws.config.annotation.EnableWs;
import org.springframework.ws.config.annotation.WsConfigurerAdapter;
import org.springframework.ws.transport.http.MessageDispatcherServlet;
import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition;
import org.springframework.xml.xsd.SimpleXsdSchema;
import org.springframework.xml.xsd.XsdSchema;

@EnableWs
@Configuration
public class WebServiceConfig extends WsConfigurerAdapter {

 @Bean
 public ServletRegistrationBean < MessageDispatcherServlet > messageDispatcherServlet(ApplicationContext applicationContext) {
  MessageDispatcherServlet servlet = new MessageDispatcherServlet();
  servlet.setApplicationContext(applicationContext);
  servlet.setTransformWsdlLocations(true);
  return new ServletRegistrationBean < > (servlet, "/ws/*");
 }

 @Bean(name = "customer")
 public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema countriesSchema) {
  DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
  wsdl11Definition.setPortTypeName("CustomerServicePort");
  wsdl11Definition.setLocationUri("/ws");
  wsdl11Definition.setTargetNamespace("http://spring.tutorialflix.com/service/v1");
  wsdl11Definition.setSchema(customerSchema());
  return wsdl11Definition;
 }

 @Bean
 public XsdSchema customerSchema() {
  return new SimpleXsdSchema(new ClassPathResource("customer-service.xsd"));
 }
}
  • portTypeName : Interface name

  • locationUri : URL to expose service

  • targetNamespace: Target name space for the WSDL elements

  • schema: Location of the schema

  • @Bean(name = "customer") :Name of this bean will be used the wsdl name.

Configure Logging Interceptors

To log the payload of our SOAP messages we'll add the below beans in the WebServiceConfig class.

        @Bean
        PayloadLoggingInterceptor payloadLoggingInterceptor() {
                return new PayloadLoggingInterceptor();
        }

        @Bean
        PayloadValidatingInterceptor payloadValidatingInterceptor() {
                final PayloadValidatingInterceptor payloadValidatingInterceptor = new PayloadValidatingInterceptor();
                payloadValidatingInterceptor.setSchema(new ClassPathResource("customer-service.xsd"));
                return payloadValidatingInterceptor;
        }

Configure Security Interceptors

XwsSecurityInterceptor will intercept the request and validate the username & password by the help of SimplePasswordValidationCallbackHandler.

For this post we are using username = admin and password = pwd123.

        @Bean
        XwsSecurityInterceptor securityInterceptor() {
                XwsSecurityInterceptor securityInterceptor = new XwsSecurityInterceptor();
                securityInterceptor.setCallbackHandler(callbackHandler());
                securityInterceptor.setPolicyConfiguration(new ClassPathResource("securityPolicy.xml"));
                return securityInterceptor;
        }

        @Bean
        SimplePasswordValidationCallbackHandler callbackHandler() {
                SimplePasswordValidationCallbackHandler callbackHandler = new SimplePasswordValidationCallbackHandler();
                callbackHandler.setUsersMap(Collections.singletonMap("admin", "pwd123"));
                return callbackHandler;
        }

Add interceptor to the chain

        @Override
        public void addInterceptors(List<EndpointInterceptor> interceptors) {
                interceptors.add(payloadLoggingInterceptor());
                interceptors.add(payloadValidatingInterceptor());
                interceptors.add(securityInterceptor());
        }

Adding the security policy

Now create a file with name securityPolicy.xml in the resources folder and add the below mentioned configuration.

<xwss:SecurityConfiguration xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
        <xwss:RequireUsernameToken passwordDigestRequired="false" nonceRequired="false" />
</xwss:SecurityConfiguration>

Bootstrap as Spring Boot Application

Add the following plugin in pom.xml to make the jar spring boot compatible.

 <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
    </configuration>
</plugin>
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

Define the main method which will allow this application to run using Spring Boot.This class should be in the root package always for the component scan.

package com.tutorialflix.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootSoapWebService {

 public static void main(String[] args) {
  SpringApplication.run(SpringBootSoapWebService.class, args);
 }
}

Deploy the service

$ mvn spring-boot:run

Now, you can see the WSDL at the following location localhost:8080/ws/customer.wsdl

Test the service

  • Import the WSDL in SOAP-UI & soap-UI will auto-generate the request structure for the request.

  • Send a request to the service

Download the code

You can clone or download this project from Github