środa, 14 marca 2012

Apache CXF vs WCF i TransportWithMessageCredentials

Apache CXF ma problem w komunikacji z Windows Communication Framework (WCF). Konkretnie chodzi o przypadek, w którym klient CXF wywołuje usługę WCF wykorzystującą schemat zabezpieczeń TransportWithMessageCredentials. W moim scenariuszu używam generacji Timestamp w połączeniu z Signature i BinarySecurityToken. Problem objawia się, gdy WCF próbuje parsować nagłówek <wsse:security/>. Wywołanie zwykle kończy się błędem:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
   <s:Body>
      <s:Fault>
         <faultcode xmlns:a="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">a:InvalidSecurity</faultcode>
         <faultstring xml:lang="pl-PL">An error occurred when verifying security for the message.</faultstring>
      </s:Fault>
   </s:Body>
</s:Envelope>

WCF zakłada określoną kolejność elementów nagłówka . To czy jest to zgodne czy nie ze specyfikacją - nie wnikam. Niestety, biblioteka wss4j, która jest używana w Apache CXF (i nie tylko, bo Axis i SOAP UI również jej używają) nie pozwala na poziomie jakiejkolwiek konfiguracji wpłynąć na kolejność elementów tego nagłówka. Został zgłoszony błąd WSS-231 do biblioteki wss4j, ale nie wygląda na to, żeby miał zostać rozwiązany w najbliższej przyszłości.

Z pomocą przychodzi mechanizm interceptorów w Apache CXF, dzięki któremu możliwa jest modyfikacja wygenerowanego komunikatu żądania na poziomie drzewa DOM. Przykładowy kod:

package devbox.cxf;

import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.binding.soap.interceptor.AbstractSoapInterceptor;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.Phase;
import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
import org.w3c.dom.Node;

import javax.xml.soap.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class FixSecurityHeaderOutInterceptor extends AbstractSoapInterceptor {

    public FixSecurityHeaderOutInterceptor() {
        super(Phase.PRE_PROTOCOL);
        getAfter().add(WSS4JOutInterceptor.class.getName());
    }

    public void handleMessage(SoapMessage message) throws Fault {
        boolean isOutbound = message == message.getExchange().getOutMessage()
                || message == message.getExchange().getOutFaultMessage();

        if (isOutbound) {
            message.getInterceptorChain().add(new FixSecurityHeaderOutEndingInterceptor());
        }
    }

    public class FixSecurityHeaderOutEndingInterceptor extends AbstractSoapInterceptor {
        public FixSecurityHeaderOutEndingInterceptor() {
            super(Phase.POST_PROTOCOL_ENDING);
        }

        public void handleMessage(SoapMessage message) throws Fault {
            try {
                SOAPPart soap = (SOAPPart) message.getContent(Node.class);
                SOAPHeader header = soap.getEnvelope().getHeader();

                // wsse:Security
                SOAPHeaderElement security = null;
                Iterator iterator = header.examineAllHeaderElements();
                while (iterator.hasNext()) {
                    SOAPHeaderElement headerElement = (SOAPHeaderElement) iterator.next();
                    if (headerElement.getNodeName().equals("wsse:Security")) {
                        security = headerElement;
                        break;
                    }
                }

                // no security - no job
                if (security == null) {
                    return;
                }

                Map<String, SOAPElement> elementMap = new HashMap<String, SOAPElement>();

                Iterator childElements = security.getChildElements();
                while (childElements.hasNext()) {
                    SOAPElement element = (SOAPElement) childElements.next();
                    elementMap.put(element.getTagName(), element);
                    element.detachNode();
                }

                security.addChildElement(elementMap.get("wsu:Timestamp"));
                security.addChildElement(elementMap.get("wsse:BinarySecurityToken"));
                security.addChildElement(elementMap.get("ds:Signature"));
            } catch (SOAPException e) {
                throw new Fault(e);
            }
        }
    }
}

Kod sprowadza się do wyłuskania elementu nagłówka, zabrania z niego wszystkich interesujących elementów i powtórne ich dodanie w kolejności akceptowalnej przez WCF. Najtrudniejsze było trafienie w odpowiednie fazy, w których ma się wykonać ten kod. Metoda prób i błędów pozwoliła trafić w dziesiątkę. Zostało jeszcze wszystko skonfigurować:

<cxf:cxfEndpoint id="cxfProducerEndpoint" xmlns:cxf="http://camel.apache.org/schema/cxf"
address="${producerEndpointURI}" wsdlURL="wsdl/SomeService.wsdl">
        <cxf:inInterceptors>
            <ref bean="producerWSS4JInInterceptor"/>
        </cxf:inInterceptors>
        <cxf:outInterceptors>
            <ref bean="producerWSS4JOutInterceptor"/>
            <bean class="devbox.cxf.FixSecurityHeaderOutInterceptor"/>
        </cxf:outInterceptors>
</cxf:cxfEndpoint>

W przykładzie celowo pominięto szczegóły konfiguracji WSS4J, są bez znaczenia. Ważne jest, aby na końcu listy interceptorów OUT umieścić ten, który naprawia rzeczony nagłówek.

2 komentarze: