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
- JDK 1.8 +
- Maven
- IDE
Approach
SOAP services can be developed with two methods
- Contract First: Define WSDL and Schema before writing any code.
- 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