In my last post I finished a handler to verify the signature in a SOAP message following a simple web service security scheme.
Next I was ready to use JAX-WS 2.0 to create a client to call my
secure service. The wsimport
tool does the work of generating
the client code, but I needed to figure out how to insert my custom
security element into the message header before it gets sent to the
server.
The process of preparing to send a message from the client should look something like this:
- Create the message body, specifying the operation and relevant data or parameters.
- Add a SOAP
Header
element to the message if it does not already have one. - Add a
Security
element to the header. - Add the
Timestamp
andAccessID
elements to theSecurity
element. - Create the
Signature
element based on the access ID, timestamp and canonicalized text of the message body and add it to theSecurity
element.
The client code that invokes a specific web service operation should only have to worry about the first step of this process. The remaining steps are the same for all operations and should be handled transparently from the viewpoint of the caller.
The message processing pipeline for JAX-WS is similar whether you are sending or receiving messages. A message always passes through a handler chain before and after the message is actually processed. In this case, I needed to create a handler to insert the security element into the message header as described above.
My signature creation handler extended the same base handler I used
on the server. In this handler the action happens in the
handleOutbound
method:
package member.client; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.logging.Logger; import org.w3c.dom.*; import javax.xml.namespace.QName; import javax.xml.ws.handler.MessageContext; import javax.xml.ws.handler.soap.SOAPHandler; import javax.xml.soap.SOAPBody; import javax.xml.soap.SOAPElement; import javax.xml.soap.SOAPException; import javax.xml.soap.SOAPMessage; import javax.xml.soap.SOAPPart; import javax.xml.soap.SOAPEnvelope; import javax.xml.soap.SOAPHeader; import javax.xml.soap.SOAPHeaderElement; import javax.xml.ws.handler.soap.SOAPMessageContext; import member.common.Canonicalizer; import member.common.Signature; import member.common.BaseHandler; import member.common.Timestamp; /* * Adds a signature to outbound messages. */ public class SignatureCreationHandler extends BaseHandler { /** * TO DO: replace these with configuration properties **/ private static final String ACCESS_ID = "karl"; private static final String SECRET_KEY = "secret"; private static final Logger log = Logger.getLogger("member"); public boolean handleOutbound(SOAPMessageContext smc) throws SOAPException { SOAPMessage message = smc.getMessage(); SOAPPart part = message.getSOAPPart(); SOAPEnvelope envelope = part.getEnvelope(); SOAPBody body = envelope.getBody(); if (body == null) { body = envelope.addBody(); } SOAPHeader header = envelope.getHeader(); if (header == null) { header = envelope.addHeader(); } addSecurityElement(header, body); return true; } private void addSecurityElement(SOAPHeader header, SOAPBody body) throws SOAPException { QName qName = getQName(body, SECURITY_ELEMENT); if (qName == null) { return; } SOAPHeaderElement securityElement = header.addHeaderElement(qName); qName = new QName("", TIMESTAMP_ELEMENT, ""); SOAPElement timestampElement = securityElement.addChildElement(qName); String timestamp = Timestamp.create(); timestampElement.addTextNode(timestamp); qName = new QName("", ACCESS_ID_ELEMENT, ""); SOAPElement clientIDElement = securityElement.addChildElement(qName); clientIDElement.addTextNode(ACCESS_ID); qName = new QName("", SIGNATURE_ELEMENT, ""); SOAPElement signatureElement = securityElement.addChildElement(qName); String bodyText = Canonicalizer.toText(body); String signedText = timestamp + ":" + ACCESS_ID + ":" + bodyText; String signature = Signature.create(SECRET_KEY, signedText); signatureElement.addTextNode(signature); } }
Once the handler was written, the next challenge was to incorporate
it into the service endpoint interface (SEI) that the
wsimport
tool generates based on a WSDL file.
The endpoint interface is what a client uses to access the
service.
To customize the endpoint interface, I created a binding file
declaring a handler-chains
element and passed it to the
wsimport
tool. The tool then added the specified nodes
to the WSDL before generating the SEI:
<bindings xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" wsdlLocation="http://localhost:8082/member?wsdl" xmlns="http://java.sun.com/xml/ns/jaxws"> <bindings node="wsdl:definitions"> <package name="member.client"/> </bindings> <bindings node="wsdl:definitions" xmlns:javaee="http://java.sun.com/xml/ns/javaee"> <javaee:handler-chains> <javaee:handler-chain> <javaee:handler> <javaee:handler-class>member.client.SignatureCreationHandler</javaee:handler-class> </javaee:handler> </javaee:handler-chain> </javaee:handler-chains> </bindings> </bindings>
After running the wsimport
tool, I was able to use the
generated classes to run a simple test program that successfully
executed an operation on the secure service:
package member.client; import java.math.BigInteger; import java.util.List; import java.rmi.RemoteException; public class Client { public static void main (String[] args) throws Exception { MemberService service = new MemberService(); MemberPort port = service.getMemberServicePort(); BigInteger memberID = new BigInteger("2"); Listsn = port.memberGroups(memberID); } }
As illustrated by this simple test program, the handler manages security transparently for the client.