01 / Goal
What this demo builds
The repository demonstrates service-to-service communication where the message itself
carries the proof of integrity and sender identity. The SOAP envelope is signed in the
wsse:Security header, so the receiver validates the XML payload instead of
trusting only the transport channel.
There are two Spring Boot applications. client-service exposes a REST API
only to make the demo easy to call from a browser, curl, or Postman. That REST API is
not part of the SOAP security model. Internally, the client sends signed SOAP requests
to provider-service. The provider returns signed SOAP responses and, after
a successful GetContent, calls a signed SOAP callback on the client.
02 / Contract first
The shared model comes from XSD
The soap-contracts module contains XSD schemas for the provider API and
the callback API. Maven generates JAXB classes from those schemas, and both services
use the same model classes for endpoints and clients. The WSDL is generated by
Spring-WS from the XSD, so there is no manually maintained WSDL duplicate.
<xs:element name="GetContentRequest">
<xs:complexType>
<xs:sequence>
<xs:element name="contentId" type="xs:string"/>
<xs:element name="callbackUrl" type="xs:string" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="GetContentResponse">
<xs:complexType>
<xs:sequence>
<xs:element name="contentId" type="xs:string"/>
<xs:element name="title" type="xs:string"/>
<xs:element name="body" type="xs:string"/>
<xs:element name="category" type="xs:string"/>
<xs:element name="servedAt" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
03 / Spring-WS
Publishing WSDL with DefaultWsdl11Definition
Each SOAP application registers Spring-WS MessageDispatcherServlet under
/ws/*. The provider publishes /ws/content.wsdl. The client
publishes /ws/callback.wsdl for the provider callback.
@Bean
ServletRegistrationBean<MessageDispatcherServlet> messageDispatcherServlet(
ApplicationContext applicationContext) {
MessageDispatcherServlet servlet = new MessageDispatcherServlet();
servlet.setApplicationContext(applicationContext);
servlet.setTransformWsdlLocations(true);
return new ServletRegistrationBean<>(servlet, "/ws/*");
}
@Bean(name = "content")
DefaultWsdl11Definition contentWsdl(XsdSchema contentSchema) {
DefaultWsdl11Definition wsdl = new DefaultWsdl11Definition();
wsdl.setPortTypeName("ContentPort");
wsdl.setLocationUri("/ws");
wsdl.setTargetNamespace("urn:demo:provider:content:v1");
wsdl.setSchema(contentSchema);
return wsdl;
}
04 / Trust
Each service owns a signing key and trusts the other certificate
The demo uses local PKCS12 stores. The client signs requests with its
private key. The provider validates them with the client certificate in
provider-truststore.p12. The provider signs responses and callbacks with
its own private key. The client validates them through client-truststore.p12.
client-service
client-signing.p12client private key
client-truststore.p12provider certificate
provider-service
provider-signing.p12provider private key
provider-truststore.p12client certificate
./scripts/generate-demo-keys.sh
# Generated files:
# provider-service/src/main/resources/security/provider-signing.p12
# provider-service/src/main/resources/security/provider-truststore.p12
# client-service/src/main/resources/security/client-signing.p12
# client-service/src/main/resources/security/client-truststore.p12
05 / WS-Security
Signing and validation with Wss4jSecurityInterceptor
The central Spring-WS integration point is Wss4jSecurityInterceptor. On
the server side it is registered as an endpoint interceptor. On the client side it is
attached to WebServiceTemplate. The demo requires both
Timestamp and Signature in every SOAP exchange.
static final String SOAP_BODY_AND_TIMESTAMP =
"{}{http://schemas.xmlsoap.org/soap/envelope/}Body;"
+ "{}{http://docs.oasis-open.org/wss/2004/01/"
+ "oasis-200401-wss-wssecurity-utility-1.0.xsd}Timestamp";
@Bean
Wss4jSecurityInterceptor clientSecurityInterceptor() throws Exception {
Wss4jSecurityInterceptor interceptor = new Wss4jSecurityInterceptor();
interceptor.setValidationActions("Timestamp Signature");
interceptor.setValidationSignatureCrypto(trustedProviderCrypto());
interceptor.setValidationTimeToLive(300);
interceptor.setTimestampStrict(true);
interceptor.setSecurementActions("Timestamp Signature");
interceptor.setSecurementUsername("client");
interceptor.setSecurementPassword("changeit");
interceptor.setSecurementSignatureCrypto(clientSigningCrypto());
interceptor.setSecurementSignatureKeyIdentifier("DirectReference");
interceptor.setSecurementSignatureParts(SOAP_BODY_AND_TIMESTAMP);
interceptor.setSecurementTimeToLive(300);
interceptor.setSecurementMustUnderstand(true);
return interceptor;
}
06 / SOAP client
WebServiceTemplateBuilder wires marshalling, security, and logging
The client does not instantiate WebServiceTemplate directly. It uses
Spring Boot's WebServiceTemplateBuilder, sets JAXB marshalling, points to
the provider URI, attaches the WSS4J interceptor, and uses an HTTP sender that is also
intercepted by Logbook.
@Bean
WebServiceTemplate providerWebServiceTemplate(
WebServiceTemplateBuilder builder,
Jaxb2Marshaller marshaller,
Wss4jSecurityInterceptor securityInterceptor,
ClientHttpRequestMessageSender logbookSoapMessageSender,
@Value("${provider.soap.uri}") String providerSoapUri) {
return builder
.setMarshaller(marshaller)
.setUnmarshaller(marshaller)
.setDefaultUri(providerSoapUri)
.messageSenders(logbookSoapMessageSender)
.interceptors(new ClientInterceptor[] { securityInterceptor })
.build();
}
public GetContentResponse getContent(String contentId, String callbackUrl) {
GetContentRequest request = new GetContentRequest();
request.setContentId(contentId);
request.setCallbackUrl(callbackUrl);
return (GetContentResponse) providerWebServiceTemplate
.marshalSendAndReceive(request);
}
07 / Endpoints
REST facade, SOAP provider, and signed callback
The REST controller exists for testing and demonstration only. It gives a simple way to trigger the SOAP flow without a SOAP UI client. The actual service-to-service contract is the Spring-WS SOAP contract, not the REST API.
REST GET /api/content/{id} reaches client-service.
The client creates GetContentRequest with a callback URL.
WSS4J signs the SOAP Body and Timestamp with the client key.
The provider validates the signature, reads content, and signs the response.
The provider sends signed NotifyContentDelivered back to the client.
The client stores the callback event and returns JSON to the test caller.
@Endpoint
public class ContentEndpoint {
private static final String NAMESPACE = "urn:demo:provider:content:v1";
@PayloadRoot(namespace = NAMESPACE, localPart = "GetContentRequest")
@ResponsePayload
public GetContentResponse getContent(@RequestPayload GetContentRequest request) {
OffsetDateTime servedAt = OffsetDateTime.now();
ContentItem item = contentCatalog.getById(request.getContentId());
callbackClient.notifyDelivered(request.getCallbackUrl(), item, servedAt);
GetContentResponse response = new GetContentResponse();
response.setContentId(item.contentId());
response.setTitle(item.title());
response.setBody(item.body());
response.setCategory(item.category());
response.setServedAt(servedAt.toString());
return response;
}
}
@RestController
@RequestMapping("/api")
public class ContentRestController {
@GetMapping("/content/{id}")
public ContentResponse getContent(@PathVariable String id) {
GetContentResponse response = providerContentClient
.getContent(id, callbackUrl());
return new ContentResponse(
response.getContentId(),
response.getTitle(),
response.getBody(),
response.getCategory(),
response.getServedAt());
}
}
08 / Observability
REST and SOAP are logged through one mechanism
The demo uses Zalando Logbook for request and response logging. Incoming REST and SOAP
traffic is visible through the servlet filter. Outgoing SOAP traffic is visible because
ClientHttpRequestMessageSender uses
LogbookClientHttpRequestInterceptor.
logging.level.org.zalando.logbook.Logbook=TRACE
logbook.format.style=http
logbook.strategy=default
logbook.write.max-body-size=-1
logbook.filters.body.default-enabled=false
@Bean
ClientHttpRequestMessageSender logbookSoapMessageSender(
LogbookClientHttpRequestInterceptor logbookInterceptor) {
InterceptingClientHttpRequestFactory requestFactory =
new InterceptingClientHttpRequestFactory(
new SimpleClientHttpRequestFactory(),
List.of(logbookInterceptor));
return new ClientHttpRequestMessageSender(requestFactory);
}
JSON pretty printing is handled by Logbook. XML pretty printing is done by a small
BodyFilter based on the JDK DOM parser and Transformer. This
keeps Logbook as the single HTTP request/response logging layer. Spring-WS
MessageTracing is intentionally not enabled.
09 / Runtime evidence
Full demo logs from client and provider
The page loads the full application logs from static files bundled with the article. They show the REST request, outgoing signed SOAP request, incoming signed SOAP response, provider-side request validation, and the signed callback exchange.
Loading client log...
Loading provider log...
10 / Run it
How to run the demo locally
The repository contains Maven Wrapper, so a local Maven installation is not required.
The .p12 files are not committed. Generate them locally before running the
services.
git clone https://github.com/emlagowski/web-service-spring.git
cd web-service-spring
./scripts/generate-demo-keys.sh
./mvnw verify
./mvnw -pl provider-service spring-boot:run
./mvnw -pl client-service spring-boot:run
curl http://localhost:8081/api/content/article-1
curl "http://localhost:8081/api/content?category=spring"
curl http://localhost:8081/api/callback-events
11 / Production notes
What changes outside a demo
This project keeps configuration close to the code to make the Spring-WS mechanics visible. In production, signing keys should come from a controlled key management process, passwords should not live in source control, certificate rotation should be planned, and signature algorithms must match current security policy.
The implementation pattern stays the same: contract-first XSD, generated JAXB model,
Spring-WS endpoints, Wss4jSecurityInterceptor on both sides, and an
explicit truststore defining whose signatures each service accepts.