I decided to write this blog entry to share my experience creating a WSSE secured SOAP web service in a Spring Boot app. I had a few minor challenges along the way so I’m hoping this will be useful to someone out there. This post assumes you already have a working knowledge of Spring Boot as well as how to create web services in a regular Spring (non – boot) Web App (NB: This tutorial uses Spring Boot 1.1.8.RELEASE).
Before we get into the code, let’s make sure we have all the necessary dependencies included. I use Apache Maven for dependency management so I have the following dependencies in my pom.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<!-- Spring Boot WS Dependencies --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-ws</artifactId> </dependency> <!-- CXF Dependencies --> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-ws-security</artifactId> <version>2.7.8</version> </dependency> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-ws-policy</artifactId> <version>2.7.8</version> </dependency> <!-- WSSE Dependencies --> <dependency> <groupId>org.apache.ws.security</groupId> <artifactId>wss4j</artifactId> <version>1.6.14</version> </dependency> |
Ok, now to the code! First, create your web service interface with the required annotations:
1 2 3 4 |
@WebService public interface MyService { @WebMethod(operationName="getServiceName", action="http://soap.api.example.com/getServiceName") String getServiceName(); } |
Notice how the operation name and SOAP action were set in the @WwbMethod annotation. If these attributes are not set, the operation name defaults to the method name and the action defaults to an empty string.
Next up is the implementation class:
1 2 3 4 5 6 7 8 |
@Service("myService") @WebService(endpointInterface = "com.example.api.soap.MyService", serviceName = "MyService") public class MyServiceImpl implements MyService { @Override public String getServiceName() { return “This is MyService”; } } |
Make sure the implementation class is also annotated with @Component or @Service and provide a name for the bean as we’ll be referencing it in our Spring config file later. Also notice the service name and endpoint interface are specified here.
Then we configure the web service servlet. The following should be placed in an application configuration file (eg. Application.java):
1 2 3 4 5 6 |
@Configuration public class WebServiceConfiguration { @Bean public ServletRegistrationBean servletRegistrationBean() { return new ServletRegistrationBean(new CXFServlet(), "/api/soap/*"); } } |
Now that the web service classes have been created and exposed to Spring, we create an XML configuration file (eg. soap-config.xml) and add the following CXF configurations:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?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:cxf="http://cxf.apache.org/core" xmlns:jaxws="http://cxf.apache.org/jaxws" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd"> <!-- Import the necessary CXF configuration files --> <import resource="classpath:META-INF/cxf/cxf.xml"/> <import resource="classpath:META-INF/cxf/cxf-extension-soap.xml"/> <import resource="classpath:META-INF/cxf/cxf-servlet.xml"/> <!-- Configure the Web Service Endpoint. Note that the "implementor" attribute is set to the bean name of the implementation class --> <jaxws:endpoint id="mySvc" implementor="#myService" address="/my-service"> </jaxws:endpoint> </beans> |
Now you should have a functional, albeit unsecured SOAP web service. To add WSSE support to the, include the following import statements and modify the JAXWS endpoint bean as shown below:
1 2 3 4 5 6 7 8 9 |
... ... ... <import resource="classpath:META-INF/cxf/cxf-extension-policy.xml"/> <import resource="classpath:META-INF/cxf/cxf-extension-ws-security.xml"/> ... ... ... <jaxws:endpoint id="mySvc" implementor="#myService" address="/my-service"> <jaxws:inInterceptors> <ref bean="wss4jIn"/> </jaxws:inInterceptors> </jaxws:endpoint> |
Add the security interceptor, specifying what type of securement you requuire:
1 2 3 4 5 6 7 8 9 |
<!-- Security interceptor requiring plain text password and timestamp --> <bean id="wss4jIn" class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor"> <constructor-arg> <map> <entry key="action" value="UsernameToken Timestamp" /> <entry key="passwordType" value="PasswordText" /> </map> </constructor-arg> </bean> |
And finally, add the following CXF bus properties:
1 2 3 4 5 6 7 |
<cxf:bus> <cxf:properties> <entry key="ws-security.validate.token" value="false" /> <entry key="ws-security.ut.no-callbacks" value="true" /> <entry key="ws-security.ut.validator" value="my.custom.CredentialValidator" /> </cxf:properties> </cxf:bus> |
Optionally, you can enable the logging feature in CXF by add the following interceptor definitions to the bus. This will write the request and response payloads to the logs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<!-- Interceptors --> <bean id="inLog" class="org.apache.cxf.interceptor.LoggingInInterceptor" /> <bean id="outLog" class="org.apache.cxf.interceptor.LoggingOutInterceptor" /> <cxf:bus> ... ... ... ... <cxf:features> <cxf:logging /> </cxf:features> <cxf:inInterceptors> <ref bean="inLog" /> </cxf:inInterceptors> <cxf:outInterceptors> <ref bean="outLog" /> </cxf:outInterceptors> <cxf:inFaultInterceptors> <ref bean="inLog" /> </cxf:inFaultInterceptors> <cxf:outFaultInterceptors> <ref bean="outLog" /> </cxf:outFaultInterceptors> </cxf:bus> |
The more keen – eyed among you may have noticed a custom validator was specified in my CXF bus properties. This can be used if you wish to validate the user’s credentials against some store such as a database. A simple validator stub follows:
1 2 3 4 5 6 7 8 9 10 11 |
public class CredentialValidator extends UsernameTokenValidator { @Override protected void verifyPlaintextPassword(UsernameToken usernameToken, RequestData data) throws WSSecurityException { try { //Connect to your data source and validate the user credentials } catch (AuthenticationException aex) { throw new WSSecurityException(WSSecurityException.FAILED_AUTHENTICATION); } } } |
And there you have it! A fully configured SOAP web service secured with WSSE and full request / response logging.