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
Headerelement to the message if it does not already have one. - Add a
Securityelement to the header. - Add the
TimestampandAccessIDelements to theSecurityelement. - Create the
Signatureelement based on the access ID, timestamp and canonicalized text of the message body and add it to theSecurityelement.
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");
List sn = port.memberGroups(memberID);
}
}
As illustrated by this simple test program, the handler manages security transparently for the client.