Basic Spring Web Service Tutorial From Contract to Security

February 25, 2010 at 4:37 am | Posted in Web Services | 23 Comments
Tags: , ,

.

Contents

Introduction

I have developed web services using the so called easy way, which is the code first approach. In this kind of technique, the xml schema and the wsdl are generated by the framework. The ones I’ve used were Metro web services and Apache CXF. Both of them uses JAX-WS annotations. It was so easy to use that you simply use annotations, deploy it to a web server, then … poom! There goes the xml schema and the wsdl generated on the fly. I didn’t care about it as long as it works! It too works very well with a client generated by wsimport from the command line.When I got a break, I checked out Spring web services which uses the conract first approach. Contract first web service approach is an industry standard best practice when developing web services.

Honestly speaking, spring web services was a lot harder to learn that JAX-WS based frameworks. It requires knowledge of SOAP, xml schema, xml frameworks, and marshallers. Despite the learning curve, the reward is so rewarding. Once you get used to programming spring web services, it will be as easy as coding a JAX-WS based framework. I’m gonna check out RESTful services soon.

After enjoying soap web service development with Spring, I decided to make my own article that will get up and running spring web services from contract to security.

About the example

I assume that you have already gone through Part 1 of spring web services documentation, especially the Chapter 3 of the reference. The example in this article is a CRUD like application using SOAP Spring web services. It will show you how to create a web service from contract to security. It uses JAXB2 un/marshalling and WSS4j for security.

The example uses the following libraries: Spring 2.5.6 Spring WS 1.5.9 jdk6 runtime The ide used was Eclipse 3.5.1 with springide pluggin. You can use any of your favorite IDES. Here’s the scenario. Our target domain is Person. The domain can be processed with methods like, AddPerson, GetPerson, GetAllPersons, UpdatePerson, and DeletePerson. The web service can only be accessed by authorized users. Unauthorized users cannot perfrom any of the service operation. To keep things simple, we’ll use an in-memory Map datasource instead of a database.

Web Service Operations

Web service uses xml to send and receive data. Some operations have both request and response. Some only have request. A soap fault becomes a response when somethings wrong with the operation such as missing Id, person not found from the server. The following operations are self explanatory.

Operation Request XML Response XML
AddPerson
<AddPersonRequest>
  <Person Id="1">
    <FirstName>Clark</FirstName>
    <LastName>Kent</LastName>
  </Person>
</AddPersonRequest>
GetPerson
<GetPersonRequest Id="1" />
<GetPersonResponse>
  <Person Id="1">
    <FirstName>Clark</FirstName>
    <LastName>Kent</LastName>
  </Person>
</GetPersonResponse>
GetAllPersons
<GetAllPersons />
<GetAllPersonResponse>
  <Person Id="1">
    <FirstName>Clark</FirstName>
    <LastName>Kent</LastName>
  </Person>
  <Person Id="2">
    <FirstName>Bruce</FirstName>
    <LastName>Wayne</LastName>
  </Person>
  <Person Id="3">
    <FirstName>Harold</FirstName>
    <LastName>Jordan</LastName>
  </Person>
</GetAllPersonsResponse>
UpdatePerson
<UpdatePersonRequest>
  <Person Id="1">
    <FirstName>Clark Joseph</FirstName>
    <LastName>Kent</LastName>
  </Person>
</UpdatePersonRequest>
DeletePerson
<DeletePersonRequest Id="1" />

The next section shows the xml schema for the xml messages.

Data Contract

Here’s where we start our contract first approach, the data contract. The wsdl contract can be happily generated by Spring’s DefaultWsdl11Definition later. There are many tools out there to generate schema from xml such as Trang command and XMLBeans’s ints2xsd. For our simple example, I personally like doing it by hand. Here’s a good tutorial for writing xml schema by hand http://www.w3schools.com/Schema/default.asp.

The resulting schema can be saved as person.xml. We’ll use this xml file later on the project setup. person.xml

<?xml version="1.0" encoding="UTF-8"?> 
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" 
  targetNamespace="http://www.example.org/person/schema" 
  xmlns:per="http://www.example.org/person/schema" 
  elementFormDefault="qualified"> 

  <xs:element name="Person"> 
    <xs:complexType> 
      <xs:sequence> 
        <xs:element name="FirstName" type="xs:string" /> 
        <xs:element name="LastName" type="xs:string" /> 
      </xs:sequence> 
      <xs:attribute ref="per:Id" use="required" /> 
    </xs:complexType> 
  </xs:element> 

  <xs:attribute name="Id" type="xs:int" /> 

  <xs:element name="AddPersonRequest"> 
    <xs:complexType> 
      <xs:sequence> 
        <xs:element ref="per:Person" /> 
      </xs:sequence> 
    </xs:complexType> 
  </xs:element> 

  <xs:element name="GetPersonRequest"> 
    <xs:complexType> 
      <xs:attribute ref="per:Id" use="required" /> 
    </xs:complexType> 
  </xs:element> 

  <xs:element name="GetPersonResponse"> 
    <xs:complexType> 
      <xs:sequence> 
        <xs:element ref="per:Person" /> 
      </xs:sequence> 
    </xs:complexType> 
  </xs:element> 

  <xs:element name="GetAllPersonsRequest"> 
    <xs:complexType> 
      <xs:sequence /> 
    </xs:complexType> 
  </xs:element> 
  
  <xs:element name="GetAllPersonsResponse"> 
    <xs:complexType> 
      <xs:sequence> 
        <xs:element ref="per:Person" maxOccurs="unbounded"/> 
      </xs:sequence> 
    </xs:complexType> 
  </xs:element> 
  
  <xs:element name="UpdatePersonRequest"> 
    <xs:complexType> 
      <xs:sequence> 
        <xs:element ref="per:Person" /> 
      </xs:sequence> 
    </xs:complexType> 
  </xs:element> 

  <xs:element name="DeletePersonRequest"> 
    <xs:complexType> 
      <xs:attribute ref="per:Id" use="required" /> 
    </xs:complexType> 
  </xs:element> 
</xs:schema>

Some of the things you should notice are the operations element. They have suffixes “Request” and “Response”. This the default suffix of Spring’s DefaultWsdl11Definition. Later on deploying the web service, the operation elements become the input/output elements. The prefix removed becomes the operation name. For example, the GetPersonElement generates this wsdl element

<wsdl:operation name="GetPerson">
  <soap:operation soapAction="" />
  <wsdl:input name="GetPersonRequest">
    <soap:body use="literal" />
  </wsdl:input>
  <wsdl:output name="GetPersonResponse">
    <soap:body use="literal" />
  </wsdl:output>
</wsdl:operation>

The next section is the eclipse project setup for the web service server. You can use any IDE you want.

Server Project Setup

1. Start a new dynamic web project named person.

2. Place person.xsd from the previous section into the WEB-INF folder.

3. Add the following jar files to WEB-INF/lib directory of the project. These jar files are (roughly) the dependencies I simplified from the echo sample. The file can be located under the lib folder of spring-2.5.6 and spring-ws-1.5.9 distribution.

    aopalliance.jar
    bcprov-jdk14-1.43.jar
    commons-logging-1.1.1.jar
    log4j-1.2.15.jar
    opensaml-1.1.jar
    saaj-impl-1.3.2.jar
    spring.jar
    spring-webmvc.jar
    spring-ws-1.5.9-all.jar
    wsdl4j-1.6.1.jar
    wss4j-1.5.8.jar
    xalan-2.7.0.jar
    xercesImpl-2.8.1.jar
    xml-apis-1.3.04.jar
    xmlsec-1.4.3.jar
    xmlsec-2.0.jar
    xws-security-2.0-FCS.jar

4. Create a new source folder named “generated” under the project folder.

5. Generate java files from person.xsd by using JAXB’s xjc command to

your project directory.

    xjc -d generated WEBContent/WEB-INF/person.xsd

You can refresh your project to view the generated files. An easier way to do it is with XJC plugin from this site https://jaxb-workshop.dev.java.net/plugins/eclipse/xjc-plugin.html

6. Create a log4j.properties file under the src folder with the following contents. These are same contents from spring ws samples.

log4j.rootLogger=WARN, stdout
log4j.logger.org.springframework.ws=DEBUG
log4j.logger.org.springframework.xml=DEBUG

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n

7. Create the service classes under the src folder.

example.service.PersonException – This will be a soap fault response

public class PersonException extends Exception {
  public PersonException(String message) {
    super(message);
  }
}

example.service.PersonService – The service interface

import java.util.List;

import org.example.person.schema.Person;

public interface PersonService {
  void addPerson(Person person) throws PersonException;
  Person getPerson(Integer id) throws PersonException;
  List<Person> getAllPersons();
  void updatePerson(Person person) throws PersonException;
  void deletePerson(Integer id) throws PersonException;
}

example.service.PersonServiceImpl – The service implementation

package example.service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.example.person.schema.Person;

public class PersonServiceImpl implements PersonService {
  private Map dataSource;

  @Override
  public void addPerson(Person person) throws PersonException {
    if (idExists(person.getId())) {
      throw new PersonException("Cannot add person. Id already exists.");
    }
    dataSource.put(person.getId(), person);
  }

  @Override
  public void deletePerson(Integer id) throws PersonException {
    if (!idExists(id)) {
      throw new PersonException("Cannot delete person. Id does not exists.");
    }
    dataSource.remove(id);
  }

  @Override
  public List<Person> getAllPersons() {
    return new ArrayList(dataSource.values());
  }

  @Override
  public Person getPerson(Integer id) throws PersonException {
    if (!idExists(id)) {
      throw new PersonException("Cannot get person. ID does not exist.");
    }
    return dataSource.get(id);
  }

  @Override
  public void updatePerson(Person person) throws PersonException {
    if (!idExists(person.getId())) {
      throw new PersonException("Cannot update person. ID does not exist.");
    }
    dataSource.put(person.getId(), person);
  }

  public void initialize() {
    dataSource = new HashMap();
    putPerson(1, "Clark", "Kent");
    putPerson(2, "Bruce", "Wayne");
    putPerson(3, "Harold", "Jordan");
  }

  private void putPerson(Integer id, String firstName, String lastName) {
    Person person = new Person();
    person.setId(id);
    person.setFirstName(firstName);
    person.setLastName(lastName);
    dataSource.put(id, person);
  }

  private boolean idExists(Integer id) {
    return dataSource.containsKey(id);
  }
}

8 .Edit web.xml. Notice the servlet name, which is spring-ws. This servlet will for the file spring-ws-servlet.xml.

WEB-INF/web.xml

<?xml version="1.0" encoding="ISO-8859-1"?> 

<!DOCTYPE web-app PUBLIC 
    "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" 
    "http://java.sun.com/dtd/web-app_2_4.dtd"> 

<web-app> 
  <display-name>Person Service</display-name> 
  <description>Simpe CRUD like Application</description> 

  <!-- Defines the Spring-WS MessageDispatcherServlet --> 
  <servlet> 
    <servlet-name>spring-ws</servlet-name> 
    <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class> 
    <init-param> 
      <!-- Transform the location attributes in WSDLs --> 
      <param-name>transformWsdlLocations</param-name> 
      <param-value>true</param-value> 
    </init-param> 
  </servlet> 

  <!-- Map all requests to this servlet --> 
  <servlet-mapping> 
    <servlet-name>spring-ws</servlet-name> 
    <url-pattern>/*</url-pattern> 
  </servlet-mapping> 
</web-app>

9. Create spring-ws-servlet.xml under the WEB-INF folder. The bean id determines the name of the wsdl. In this configuration, the bean id of the wsdl is “Person” so we can access the wsdl with this url from http://localhost:8080/person/Person.wsdl

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:oxm="http://www.springframework.org/schema/oxm"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-1.5.xsd">
	
<bean id="personService" class="example.service.PersonServiceImpl" init-method="initialize" />
	<bean id="Person" class="org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition">
		<property name="schema" ref="schema" />
		<property name="portTypeName" value="Person" />
		<property name="locationUri" value="/services" />
		<property name="targetNamespace" value="http://www.example.org/person/schema" />
	</bean>
	<bean id="schema" class="org.springframework.xml.xsd.SimpleXsdSchema">
		<property name="xsd" value="/WEB-INF/person.xsd" />
	</bean>
</beans>

The resulting directory structure should be similar with this image.

By now you can deploy the project to an application server and view the wsdl from http://localhost:8080/person/Person.wsdl. Make sure that the operations are presents. These operations can’t do anything yet. The following section shows how to make this operations work implementing and mapping the endpoint.

Implementing the endpoint

Each wsdl operation may be an input or output. We only need to map the request most of the time and the framework takes care of the response.

Our base class, AbstractPersonEndpoint extends AbstractMarshallingPayloadEndpoint and has its own personService property. The properties: personService, marshaller and unmarshaller will be autowired by spring. Our endpoints will extend our base class.

example.endpoint.AbstractPersonEndpoint

package example.endpoint;

import org.springframework.ws.server.endpoint.AbstractMarshallingPayloadEndpoint;

import example.service.PersonService;

public abstract class AbstractPersonEndpoint extends AbstractMarshallingPayloadEndpoint {
  protected PersonService personService;

  public void setPersonService(PersonService personService) {
    this.personService = personService;
  }
  protected abstract Object invokeInternal(Object request) throws Exception;
}

example.endpoint.AddPersonEndpoint

package example.endpoint;

import org.example.person.schema.AddPersonRequest;

public class AddPersonEndpoint extends AbstractPersonEndpoint {
  @Override
  protected Object invokeInternal(Object request) throws Exception {
    AddPersonRequest addPersonRequest = (AddPersonRequest) request;
    personService.addPerson(addPersonRequest.getPerson());
    return null;
  }
}

example.endpoint.DeletePersonEndpoint

package example.endpoint;

import org.example.person.schema.DeletePersonRequest;

public class DeletePersonEndpoint extends AbstractPersonEndpoint {
  @Override
  protected Object invokeInternal(Object request) throws Exception {
    DeletePersonRequest deletePersonRequest = (DeletePersonRequest) request;
    personService.deletePerson(deletePersonRequest.getId());
    return null;
  }
}

example.endpoint.GetAllPersonsEndpoint

package example.endpoint;

import org.example.person.schema.GetAllPersonsResponse;
import org.example.person.schema.Person;

public class GetAllPersonsEndpoint extends AbstractPersonEndpoint {
  @Override
  protected Object invokeInternal(Object request) throws Exception {
    GetAllPersonsResponse getAllPersonsResponse = new GetAllPersonsResponse();
    for (Person person : personService.getAllPersons()) {
      getAllPersonsResponse.getPerson().add(person);
    }
    return getAllPersonsResponse;
  }
}

example.endpoint.GetPersonEndpoint

package example.endpoint;

import org.example.person.schema.GetPersonRequest;
import org.example.person.schema.GetPersonResponse;

public class GetPersonEndpoint extends AbstractPersonEndpoint {
  @Override
  protected Object invokeInternal(Object request) throws Exception {
    GetPersonRequest getPersonRequest = (GetPersonRequest) request;
    GetPersonResponse getPersonResponse = new GetPersonResponse();
    getPersonResponse.setPerson(personService.getPerson(getPersonRequest.getId()));
    return getPersonResponse;
  }
}

example.endpoint.UpdatePersonEndpoint

package example.endpoint;

import org.example.person.schema.UpdatePersonRequest;

public class UpdatePersonEndpoint extends AbstractPersonEndpoint {
  @Override
  protected Object invokeInternal(Object request) throws Exception {
    UpdatePersonRequest updatePersonRequest = (UpdatePersonRequest) request;
    personService.updatePerson(updatePersonRequest.getPerson());
    return null;
  }
}

One thing you’ll notice is the method signature of invokeInternal method. The argument is usually the request object and the return is the response object. The request and response objects are those generated by JAXB. Yes, those with -Request and -Response suffixes. How these classes were mapped will be shown the next section.

Endpoint Mapping

Mapping the endpoints is like mapping the input operation to spring bean end point. The endpoints will have the common properties: personService, marshaller, and unmarshaller. There are different ways to declare jaxb un/marshallers, see http://static.springsource.org/spring-ws/sites/1.5/reference/html/oxm.html#oxm-jaxb2.

JAXB2 marshaller and unmarshaller are declared with the following bean definition. It uses xml-schema based configuration. The context path is the package name of JAXB generated classes.

  <oxm:jaxb2-marshaller id="marshaller" contextPath="org.example.person.schema" />
  <oxm:jaxb2-marshaller id="unmarshaller" contextPath="org.example.person.schema" />

Each of the endpoints are declared with the following bean definition and properties autowired by name.

  <bean id="addPersonEndpoint" class="example.endpoint.AddPersonEndpoint" autowire="byName"/>
  <bean id="deletePersonEndpoint" class="example.endpoint.DeletePersonEndpoint" autowire="byName"/>
  <bean id="getAllPersonsEndpoint" class="example.endpoint.GetAllPersonsEndpoint" autowire="byName"/>
  <bean id="getPersonEndpoint" class="example.endpoint.GetPersonEndpoint" autowire="byName"/>
  <bean id="updatePersonEndpoint" class="example.endpoint.UpdatePersonEndpoint" autowire="byName"/>

Interceptors

In addtion to just mapping the endpoints, we declare interceptors for validating schema , logging the xml messages, and mapping the server exceptions.

The validatingInterceptor validates rquest and response xml against the schema.

  <bean id="validatingInterceptor" class="org.springframework.ws.soap.server.endpoint.interceptor.PayloadValidatingInterceptor">
    <property name="xsdSchema" ref="schema" />
    <property name="validateRequest" value="true" />
    <property name="validateResponse" value="true" />
  </bean>

The loggingIinterceptor logs xml messages to the console.

  <bean id="loggingInterceptor" class="org.springframework.ws.server.endpoint.interceptor.PayloadLoggingInterceptor" />

Mapping request to beans

Now, the putting the endpoints, interceptors and mapping altogether. The key is the {namespace}request and the value is the spring bean endpoint that processes the request.

<bean name="endpointMapping" class="org.springframework.ws.server.endpoint.mapping.PayloadRootQNameEndpointMapping">
  <property name="interceptors">
    <list>
      <ref local="loggingInterceptor" />
      <ref local="validatingInterceptor" />
    </list>
  </property>
  <property name="mappings">
    <props>
      <prop key="{http://www.example.org/person/schema}AddPersonRequest">addPersonEndpoint</prop>
      <prop key="{http://www.example.org/person/schema}DeletePersonRequest">deletePersonEndpoint</prop>
      <prop key="{http://www.example.org/person/schema}GetAllPersonsRequest">getAllPersonsEndpoint</prop>
      <prop key="{http://www.example.org/person/schema}GetPersonRequest">getPersonEndpoint</prop>
      <prop key="{http://www.example.org/person/schema}UpdatePersonRequest">updatePersonEndpoint</prop>
    </props>
  </property>
</bean>

Exception Resolver

Well resolve exceptions as soap faults. We don’t want a stack of stack traces to be sent to the client when an exception occurs. We just send the error code and the error message.

<bean id="exceptionResolver" class="org.springframework.ws.soap.server.endpoint.SoapFaultMappingExceptionResolver">
  <property name="defaultFault" value="SERVER" />
  <property name="exceptionMappings">
    <props>
      <prop key="org.springframework.oxm.ValidationFailureException">CLIENT,Invalid request</prop>
      <prop key="example.service.PersonException">SERVER</prop>
    </props>
  </property>
</bean>

The updated spring-ws-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:oxm="http://www.springframework.org/schema/oxm"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-1.5.xsd">

  <bean id="personService" class="example.service.PersonServiceImpl" init-method="initialize" />

  <bean id="Person" class="org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition">
    <property name="schema" ref="schema" />
    <property name="portTypeName" value="Person" />
    <property name="locationUri" value="/services" />
    <property name="targetNamespace" value="http://www.example.org/person/schema" />
  </bean>

  <bean id="schema" class="org.springframework.xml.xsd.SimpleXsdSchema">
    <property name="xsd" value="/WEB-INF/person.xsd" />
  </bean>

  <bean id="validatingInterceptor" class="org.springframework.ws.soap.server.endpoint.interceptor.PayloadValidatingInterceptor">
    <property name="xsdSchema" ref="schema" />
    <property name="validateRequest" value="true" />
    <property name="validateResponse" value="true" />
  </bean>

  <bean id="loggingInterceptor" class="org.springframework.ws.server.endpoint.interceptor.PayloadLoggingInterceptor" />

  <oxm:jaxb2-marshaller id="marshaller" contextPath="org.example.person.schema" />
  <oxm:jaxb2-marshaller id="unmarshaller" contextPath="org.example.person.schema" />

  <bean id="addPersonEndpoint" class="example.endpoint.AddPersonEndpoint" autowire="byName" />
  <bean id="deletePersonEndpoint" class="example.endpoint.DeletePersonEndpoint" autowire="byName" />
  <bean id="getAllPersonsEndpoint" class="example.endpoint.GetAllPersonsEndpoint" autowire="byName" />
  <bean id="getPersonEndpoint" class="example.endpoint.GetPersonEndpoint" autowire="byName" />
  <bean id="updatePersonEndpoint" class="example.endpoint.UpdatePersonEndpoint" autowire="byName" />


  <bean name="endpointMapping" class="org.springframework.ws.server.endpoint.mapping.PayloadRootQNameEndpointMapping">
    <property name="interceptors">
      <list>
        <ref local="loggingInterceptor" />
        <ref local="validatingInterceptor" />
      </list>
    </property>
    <property name="mappings">
      <props>
        <prop key="{http://www.example.org/person/schema}AddPersonRequest">addPersonEndpoint</prop>
        <prop key="{http://www.example.org/person/schema}DeletePersonRequest">deletePersonEndpoint</prop>
        <prop key="{http://www.example.org/person/schema}GetAllPersonsRequest">getAllPersonsEndpoint</prop>
        <prop key="{http://www.example.org/person/schema}GetPersonRequest">getPersonEndpoint</prop>
        <prop key="{http://www.example.org/person/schema}UpdatePersonRequest">updatePersonEndpoint</prop>
      </props>
    </property>
  </bean>

  <bean id="exceptionResolver" class="org.springframework.ws.soap.server.endpoint.SoapFaultMappingExceptionResolver">
    <property name="defaultFault" value="SERVER" />
    <property name="exceptionMappings">
      <props>
        <prop key="org.springframework.oxm.ValidationFailureException">CLIENT,Invalid request</prop>
        <prop key="example.service.PersonException">SERVER</prop>
      </props>
    </property>
  </bean>
</beans>

The web service now has functional endpoints. We can deploy this project to a web server.

The next section shows how to invoke the web service using SOAP UI and spring ws client.

Invoking the web service

SOAP UI

The following screen shots the web service invoked with SOAP UI. There is a free version from their site www.soapui.org.

GetPerson successfully invoked.

DeletePerson fails because id does not exist.

Spring WS client

The next section shows how to make a client using spring ws client framework.

Client Project Setup

We’ll use spring-ws to code the client. The client also uses JAXB2 marshalling and unmarshalling.

1. Create a new java project.

2. Create a new source folder named generated under the project folder.

3. Copy the generated classes from the server project to the generated folder. Results are the same if you reexecute xjc against the schema.

4. Create a log4j.properties under the default package of the source folder with the following properties. We turned off the ws logs so it will not clutter the console.

log4j.rootLogger=WARN, stdout
log4j.logger.org.springframework.ws=OFF
log4j.logger.org.springframework.xml=DEBUG

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n

5. Add the following dependencies to the project

    commons-httpclient-3.1.jar     commons-logging-1.1.1.jar
    log4j-1.2.15.jar               spring.jar
    spring-ws-1.5.9-all.jar        wss4j-1.5.8.jar
    xmlsec-1.4.3.jar

6. Create package example.client under the src folder. Create applicationContext.xml

The resulting directory structure should be similar with this screen shot.

Writing the Web Service client

example.client.PersonClient – The client interface

package example.client;

import java.util.List;
import org.example.person.schema.Person;

public interface PersonClient {
  void addPerson(Person person);
  Person getPerson(Integer id);
  List<Person> getAllPersons();
  void updatePerson(Person person);
  void deletePerson(Integer id);
}

example.client.PersonClientImpl -Implementing class. This class extends WebServiceGatewaySupport which has properties like webserviceTemplate, marshaller, unmarshaller, and interceptors which will be injected by spring.

package example.client;

import java.util.List;

import org.example.person.schema.AddPersonRequest;
import org.example.person.schema.DeletePersonRequest;
import org.example.person.schema.GetAllPersonsRequest;
import org.example.person.schema.GetAllPersonsResponse;
import org.example.person.schema.GetPersonRequest;
import org.example.person.schema.GetPersonResponse;
import org.example.person.schema.Person;
import org.example.person.schema.UpdatePersonRequest;
import org.springframework.ws.client.core.support.WebServiceGatewaySupport;

public class PersonClientImpl extends WebServiceGatewaySupport implements PersonClient {

  @Override
  public void addPerson(Person person) {
    AddPersonRequest request = new AddPersonRequest();
    request.setPerson(person);
    getWebServiceTemplate().marshalSendAndReceive(request);
  }

  @Override
  public void deletePerson(Integer id) {
    DeletePersonRequest request = new DeletePersonRequest();
    request.setId(id);
    getWebServiceTemplate().marshalSendAndReceive(request);
  }

  @Override
  public List<Person> getAllPersons() {
    GetAllPersonsRequest request = new GetAllPersonsRequest();
    GetAllPersonsResponse response = (GetAllPersonsResponse) getWebServiceTemplate()
        .marshalSendAndReceive(request);
    return response.getPerson();
  }

  @Override
  public Person getPerson(Integer id) {
    GetPersonRequest request = new GetPersonRequest();
    request.setId(id);
    GetPersonResponse response = (GetPersonResponse) getWebServiceTemplate()
        .marshalSendAndReceive(request);
    return response.getPerson();
  }

  @Override
  public void updatePerson(Person person) {
    UpdatePersonRequest request = new UpdatePersonRequest();
    request.setPerson(person);
    getWebServiceTemplate().marshalSendAndReceive(request);
  }
}

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:oxm="http://www.springframework.org/schema/oxm"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-1.5.xsd">

  <bean name="webserviceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
    <property name="defaultUri" value="http://localhost:8080/person" />
    <property name="marshaller" ref="marshaller" />
    <property name="unmarshaller" ref="unmarshaller" />
  </bean>

  <oxm:jaxb2-marshaller id="marshaller" contextPath="org.example.person.schema" />
  <oxm:jaxb2-marshaller id="unmarshaller" contextPath="org.example.person.schema" />

  <bean id="client" class="example.client.PersonClientImpl">
    <property name="webServiceTemplate" ref="webserviceTemplate" />
  </bean>
</beans>

example.client.Main – This will test our client

package example.client;

import java.util.List;

import org.example.person.schema.Person;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.ws.soap.client.SoapFaultClientException;

public class Main {
  public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    PersonClient client = (PersonClient) ctx.getBean("client");
    Person person = null;

    // Add person
    try {
      person = createPerson(5, "Lex", "Luthor");
      System.out.println("Add this person " + makeString(person));
      client.addPerson(person);
    } catch (SoapFaultClientException se) {
      System.out.println("\t" + se.getMessage());
    }

    // Get person
    try {
      int id = 1;
      System.out.println("Get person with id=" + id + "...");
      person = client.getPerson(id);
      System.out.println("\tPerson Response " + makeString(person));
    } catch (SoapFaultClientException se) {
      System.out.println("\t" + se.getMessage());
    }

    // Update the person
    try {
      System.out.println("Update person's first name");
      person.setFirstName("Clark Joseph");
      client.updatePerson(person);
      System.out.println("\tsucessfully invoked update.");
    } catch (SoapFaultClientException se) {
      System.out.println("\t" + se.getMessage());
    }

    // Delete person
    try {
      int id = 9999;
      System.out.println("Delete person with id=" + id);
      client.deletePerson(id);
    } catch (SoapFaultClientException se) {
      System.out.println("\t" + se.getMessage());
    }
    // Get all persons
    System.out.println("Get all persons...");
    List<Person> persons = client.getAllPersons();
    for (Person p : persons) {
      System.out.println("\t" + makeString(p));
    }
  }

  private static Person createPerson(int id, String firstName, String lastName) {
    Person person = new Person();
    person.setId(id);
    person.setFirstName(firstName);
    person.setLastName(lastName);
    return person;
  }

  private static String makeString(Person p) {
    return "[id=" + p.getId() + ", firstName=" + p.getFirstName() + ", lastName="
        + p.getLastName() + "]";
  }
}

Running the client

Execute the example.client.Main. The console prints text similar to these.

Get person with id=1...
	Person Response [id=1, firstName=Clark, lastName=Kent]
Update person's first name
	sucessfully invoked update.
Delete person with id=9999
	Cannot delete person. Id does not exists.
Get all persons...
	[id=1, firstName=Clark Joseph, lastName=Kent]
	[id=2, firstName=Bruce, lastName=Wayne]
	[id=3, firstName=Harold, lastName=Jordan]
	[id=5, firstName=Lex, lastName=Luthor]

By now, the web service and the client works well. The next sections adds security the web service wherein it requires the client to include a username token.

.

Securing the Web Service

Server

In our example we’ll use WSS4J security. More information on how use use WSS4j security can be found at spring ws documetation.

http://static.springsource.org/spring-ws/sites/1.5/reference/html/security.html#security-wss4j-security-interceptor.

Add the these bean definitions in the server’s spring-ws-servlet.xml.

  <bean id="wsSecurityInterceptor" class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="UsernameToken" />
    <property name="validationCallbackHandler" ref="callbackHandler" />
  </bean>

  <bean id="callbackHandler" class="org.springframework.ws.soap.security.wss4j.callback.SimplePasswordValidationCallbackHandler">
    <property name="users">
      <props>
        <prop key="Bert">Ernie</prop>
        <prop key="Mickey">Mouse</prop>
      </props>
    </property>
  </bean>

The configured the wsSecurityInterceptor with the following properties.

validationActions = UserNameToken. This requires the request to have username tokens in the header.

validationCallbackHandler calls a reference to the bean, callbackHandler.

The callbackHandler bean is SimplePasswordCallBackHandlerwhich is configured to handle an in-memory collection of users and passwords in clear text.

Add wsSecurityInterceptor to the list of interceptors of the endpoint mapping

  <bean name="endpointMapping" class="org.springframework.ws.server.endpoint.mapping.PayloadRootQNameEndpointMapping">
    <property name="interceptors">
      <list>
        <ref local="loggingInterceptor" />
        <ref local="validatingInterceptor" />
        <ref local="wsSecurityInterceptor" />
      </list>
    </property>
...
  </bean>

That’s it. Security is just a matter of configuration.

The updated spring-ws-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:oxm="http://www.springframework.org/schema/oxm"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-1.5.xsd">

  <bean id="personService" class="example.service.PersonServiceImpl" init-method="initialize" />

  <bean id="Person" class="org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition">
    <property name="schema" ref="schema" />
    <property name="portTypeName" value="Person" />
    <property name="locationUri" value="/services" />
    <property name="targetNamespace" value="http://www.example.org/person/schema" />
  </bean>

  <bean id="schema" class="org.springframework.xml.xsd.SimpleXsdSchema">
    <property name="xsd" value="/WEB-INF/person.xsd" />
  </bean>

  <bean id="validatingInterceptor" class="org.springframework.ws.soap.server.endpoint.interceptor.PayloadValidatingInterceptor">
    <property name="xsdSchema" ref="schema" />
    <property name="validateRequest" value="true" />
    <property name="validateResponse" value="true" />
  </bean>

  <bean id="loggingInterceptor" class="org.springframework.ws.server.endpoint.interceptor.PayloadLoggingInterceptor" />

  <oxm:jaxb2-marshaller id="marshaller" contextPath="org.example.person.schema" />
  <oxm:jaxb2-marshaller id="unmarshaller" contextPath="org.example.person.schema" />

  <bean id="addPersonEndpoint" class="example.endpoint.AddPersonEndpoint" autowire="byName" />
  <bean id="deletePersonEndpoint" class="example.endpoint.DeletePersonEndpoint" autowire="byName" />
  <bean id="getAllPersonsEndpoint" class="example.endpoint.GetAllPersonsEndpoint" autowire="byName" />
  <bean id="getPersonEndpoint" class="example.endpoint.GetPersonEndpoint" autowire="byName" />
  <bean id="updatePersonEndpoint" class="example.endpoint.UpdatePersonEndpoint" autowire="byName" />


  <bean name="endpointMapping" class="org.springframework.ws.server.endpoint.mapping.PayloadRootQNameEndpointMapping">
    <property name="interceptors">
      <list>
        <ref local="loggingInterceptor" />
        <ref local="validatingInterceptor" />
        <ref local="wsSecurityInterceptor" />
      </list>
    </property>
    <property name="mappings">
      <props>
        <prop key="{http://www.example.org/person/schema}AddPersonRequest">addPersonEndpoint</prop>
        <prop key="{http://www.example.org/person/schema}DeletePersonRequest">deletePersonEndpoint</prop>
        <prop key="{http://www.example.org/person/schema}GetAllPersonsRequest">getAllPersonsEndpoint</prop>
        <prop key="{http://www.example.org/person/schema}GetPersonRequest">getPersonEndpoint</prop>
        <prop key="{http://www.example.org/person/schema}UpdatePersonRequest">updatePersonEndpoint</prop>
      </props>
    </property>
  </bean>

  <bean id="exceptionResolver" class="org.springframework.ws.soap.server.endpoint.SoapFaultMappingExceptionResolver">
    <property name="defaultFault" value="SERVER" />
    <property name="exceptionMappings">
      <props>
        <prop key="org.springframework.oxm.ValidationFailureException">CLIENT,Invalid request</prop>
        <prop key="example.service.PersonException">SERVER</prop>
      </props>
    </property>
  </bean>

  <bean id="wsSecurityInterceptor" class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="validationActions" value="UsernameToken" />
    <property name="validationCallbackHandler" ref="callbackHandler" />
  </bean>

  <bean id="callbackHandler" class="org.springframework.ws.soap.security.wss4j.callback.SimplePasswordValidationCallbackHandler">
    <property name="users">
      <props>
        <prop key="Bert">Ernie</prop>
        <prop key="Mickey">Mouse</prop>
      </props>
    </property>
  </bean>
</beans>

If you try to run the previous client, you’ll get an output like this.

Add this person [id=5, firstName=Lex, lastName=Luthor]
	No WS-Security header found
Get person with id=1...
	No WS-Security header found
Update person's first name
	No WS-Security header found
Delete person with id=9999
	No WS-Security header found
Get all persons...
Exception in thread "main" org.springframework.ws.soap.client.SoapFaultClientException: No WS-Security header found

The next section shows how to add security access to the client using wss4j as well.

Client

Client security is mostly configuration of the application context.

Add this bean to the client’s applicationContext.xml.

  <bean id="wsSecurityInterceptor" class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="UsernameToken"/>
  </bean>

Only the bean’s securement action is defined. Although we can hard code the securementUsername and the securementPassword, well do this on the runtime when the program has to switch usernames to test against the server’s list of usernames.

Add wsSecrityInterceptor to the webserviceTemplate’s list of interceptor.

  <bean name="webserviceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
    <property name="defaultUri" value="http://localhost:8080/person" />
    <property name="marshaller" ref="marshaller" />
    <property name="unmarshaller" ref="unmarshaller" />
    <property name="interceptors">
      <list>
        <ref local="wsSecurityInterceptor" />
      </list>
    </property>
  </bean>

The updated applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:oxm="http://www.springframework.org/schema/oxm"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-1.5.xsd">

  <bean name="webserviceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
    <property name="defaultUri" value="http://localhost:8080/person" />
    <property name="marshaller" ref="marshaller" />
    <property name="unmarshaller" ref="unmarshaller" />
    <property name="interceptors">
      <list>
        <ref local="wsSecurityInterceptor" />
      </list>
    </property>
  </bean>

  <oxm:jaxb2-marshaller id="marshaller" contextPath="org.example.person.schema" />
  <oxm:jaxb2-marshaller id="unmarshaller" contextPath="org.example.person.schema" />

  <bean id="client" class="example.client.PersonClientImpl">
    <property name="webServiceTemplate" ref="webserviceTemplate" />
  </bean>

  <bean id="wsSecurityInterceptor" class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">
    <property name="securementActions" value="UsernameToken"/>
  </bean>
</beans>

example.client.Main – This will test the client with security. The main class has a method setUsernameToken that allows the securityInterceptor to change username and password.

package example.client;

import org.example.person.schema.Person;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.ws.soap.client.SoapFaultClientException;
import org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor;

public class Main {
  public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    PersonClient client = (PersonClient) ctx.getBean("client");
    
    Wss4jSecurityInterceptor interceptor = (Wss4jSecurityInterceptor) ctx.getBean("wsSecurityInterceptor");
   
    System.out.println("Bert is authorized");
    setUsernameToken(interceptor, "Bert", "Ernie");    
    doGetPerson(client, 1);
    
    System.out.println("Bert got wrong password");
    setUsernameToken(interceptor, "Bert", "Big Bird");
    doGetPerson(client, 1);
    
    System.out.println("Mickey is also authorized");
    setUsernameToken(interceptor, "Mickey", "Mouse");
    doGetPerson(client, 2);
    
    System.out.println("Batman is not authorized");
    setUsernameToken(interceptor, "Batman", "Robin");
    doGetPerson(client, 2);
  }

  private static void doGetPerson(PersonClient client, int id) {
    try {
      Person person = null;
      System.out.println("Get person with id=" + id + "...");
      person = client.getPerson(id);
      System.out.println("\tPerson Response " + makeString(person));
    } catch (SoapFaultClientException se) {
      System.out.println("\t" + se.getMessage());
    }
  }
  
  private static String makeString(Person p) {
    return "[id=" + p.getId() + ", firstName=" + p.getFirstName() + ", lastName=" + p.getLastName() + "]";
  }
  
  private static void setUsernameToken(Wss4jSecurityInterceptor interceptor, String user, String pass) {
    interceptor.setSecurementUsername(user);
    interceptor.setSecurementPassword(pass);
  }
}

If you run the program, the console prints something like this.

Bert is authorized
Get person with id=1...
	Person Response [id=1, firstName=Clark, lastName=Kent]
Bert got wrong password
Get person with id=1...
	The security token could not be authenticated or authorized; nested exception is
org.apache.ws.security.WSSecurityException: The security token could not be authenticated or authorized
Mickey is also authorized
Get person with id=2...
	Person Response [id=2, firstName=Bruce, lastName=Wayne]
Batman is not authorized
Get person with id=2...
	The security token could not be authenticated or authorized; nested exception is
org.apache.ws.security.WSSecurityException: The security token could not be authenticated or authorized

Download PDF and Source

person-ws pdf – PDF equivalent of this article

person-ws-source.zip - Eclipse project sources. Just get the jar files from the lib folder of spring 2.5.6 and spring-ws 1.5.9

About these ads

23 Comments »

RSS feed for comments on this post. TrackBack URI

  1. Excellent tutorial.

    However I’m not able to run it succesfully because it seems (see below) that Maven can’t find the dependency that spring ws security has on xws-security.

    Any ideas what might be causing this problem ?
    Any ideas how to fix it ?
    Can you please also publish your project’s pom.xml file(s) ?

    Thanks,
    EDH

    5/06/10 9:47:36 CEST: Downloading http://mirrors.ibiblio.org/pub/mirrors/maven2/org/springframework/ws/spring-ws-security/1.5.9/spring-ws-security-1.5.9-sources.jar
    5/06/10 9:47:36 CEST: Downloaded http://mirrors.ibiblio.org/pub/mirrors/maven2/org/springframework/ws/spring-ws-security/1.5.9/spring-ws-security-1.5.9-sources.jar
    5/06/10 9:47:37 CEST: Downloaded sources for org.springframework.ws:spring-ws-security:1.5.9
    5/06/10 9:47:38 CEST: Maven Builder: AUTO_BUILD
    5/06/10 9:51:21 CEST: Refreshing [/codebase/pom.xml, /led/pom.xml, /led-basis-ws/pom.xml]
    5/06/10 9:51:22 CEST: Missing artifact com.sun.xml.wss:xws-security:jar:2.0-FCS:compile
    5/06/10 9:51:23 CEST: Maven Builder: AUTO_BUILD
    5/06/10 9:54:51 CEST: Refreshing [/codebase/pom.xml, /led/pom.xml, /led-basis-ws/pom.xml]
    5/06/10 9:54:52 CEST: [WARN] Missing POM for com.sun.xml.wss:xws-security:jar:2.0-FCS
    5/06/10 9:54:52 CEST: Missing artifact com.sun.xml.wss:xws-security:jar:2.0-FCS:compile

  2. I had a difficult time locating the required jar files but at last it worked.

    Very nice tutorial on spring web service. Your instructor-like approach got me starting with spring web service.

    Thanks

  3. Very excellent tutorial! Thank you very much, you saved the day!

  4. excellent tutorial, thanks aloot for sharing this one. few observations or I may say changes that I have done to make this tutorial run in my environment are:-

    1. autowire attribute in service endpoint was not working for me, so I wrote the specific property which were needs to be injected, such as
    –marshaller and unmarshaller. Please note in such case you need to define jaxb2-marshaller as a bean. for example

    com.experian.sws.generated

    hence the endpoint tag would now look like

    2. since I was using JBoss-4.2.3 as container, following JARs need to be put under server->->lib.
    -spring.jar
    -spring-ws-1.5.9-all.jar
    -spring-webmvc.jar

    Rohit Anand

    • Hi Rohit,

      I am trying to learn SpringWS. I am getting the following excaption
      org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘Person’ defined in ServletContext resource .
      Looks like you made a change. Could you please share what you did to make this work.

      Thanks in advance.

  5. Jerome very superb tutorial. It really help me a lot in my new job. Thanks. More power. God bless always.

  6. Excellent !
    Thanks for your tutorial.

  7. terrific tutorial – Thank you so much!!!

  8. Very nice tutorial !
    It was nice to find a tutorial without using Maven.
    A lot of tutorials are using Maven but doesn’t give exactly the good list of jars/dependencies and it was very difficult to know which jars must be used and why it doesn’t work. I have spent a lot of time to deal with version problems.
    Thank you for your tutorial :-)

  9. Excellent Tutorial…

    It works fin in my local. But When I am moving Service on server after adding Wss4jSecurityInterceptor its giving me following exception when I am calling from local. If am removing Interceptor then its gives proper response but below error if I add Wss4jSecurityInterceptor on
    PayloadRootAnnotationMethodEndpointMapping. Is it because of HTTPS ?

    org.springframework.ws.client.WebServiceTransportException: Internal Server Error [500]
    at org.springframework.ws.client.core.WebServiceTemplate.handleError(WebServiceTemplate.java:627)
    at org.springframework.ws.client.core.WebServiceTemplate.doSendAndReceive(WebServiceTemplate.java:551)
    at org.springframework.ws.client.core.WebServiceTemplate.sendAndReceive(WebServiceTemplate.java:502)
    at org.springframework.ws.client.core.WebServiceTemplate.marshalSendAndReceive(WebServiceTemplate.java:351)
    at org.springframework.ws.client.core.WebServiceTemplate.marshalSendAndReceive(WebServiceTemplate.java:345)
    at org.springframework.ws.client.core.WebServiceTemplate.marshalSendAndReceive(WebServiceTemplate.java:337)

  10. Excellent tutorial! Everything worked exactly as you described. I’d like now instead of username token use SAML token. Please let me know if you have any suggestions. Thank you very much! Alex

  11. Excellent example. Everything worked first time. I have been trying to get examples of spring ws working for days.
    I have now been asked to look at calling a third party ws using their wsdl. Since your example generates the wsdl from the schema, how can I use spring ws to generate the classes that I receive to call the third party web service.
    Thanks for all your help.

    • you can still use xjc from jdk 6.
      This example generates classes from Amazon e-commerce
      xjc -verbose -wsdl http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl

  12. Great tutorial and examples.. This example is much easier than the examples or tutorials from spring-ws

  13. Hi Jerome,

    Nice tutorial…………

  14. thanks Jerome (merci) very clear and detailed example. I hope that you would post more example

  15. very Good Example

  16. Excellent example with excellent explaination.

  17. Superb…..Even we can find many tutorials on WebServices,this is simple,clearly explained and very very informative..Thanks for this nice tutorial…

  18. Nice tuto. Thanks a lot

  19. Very useful and superb example….Thanks a lot for having posted such a useful examples

  20. thanks. expecting more examples from u :)


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com. | The Pool Theme.
Entries and comments feeds.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: