вторник, 30 апреля 2013 г.

Вызов WCF веб-сервиса, использующего WS-Security

Недавно потребовалось вызвать .net WCF веб-сервис из java программы. При этом исходный сервис использовал WS-Security с подписью и шифрованием. Раньше мне не приходилось использовать WS-Security в веб-сервисах и после некоторого периода исследований я остановился на двух библиотеках, которые декларировали поддержку данной технологии: Apache CXF и Glassfish Metro. Первым я попробовал CXF.
Документации не сказать чтобы совсем никакой, но по части WS_Security очень скудная, и не понятно, как же в итоге заставить клиента веб-сервиса работать. Забегая вперед скажу, что у меня так и не получилось создать запрос, который был бы принят сервером.

Вот пример попытки вызова метода веб-сервиса.

        Client client = ClientProxy.getClient(iface);
        client.getInInterceptors().add(new LoggingInInterceptor());
        client.getOutInterceptors().add(new LoggingOutInterceptor());
        Endpoint cxfEndpoint = client.getEndpoint();
        cxfEndpoint.put("ws-security.encryption.username", "server");
        cxfEndpoint.put("ws-security.encryption.properties", "enc.properties");
        cxfEndpoint.put("ws-security.signature.username", "mykey");
        cxfEndpoint.put("ws-security.signature.properties", "sig.properties");

        cxfEndpoint.put("ws-security.callback-handler", "ClientKeystorePasswordCallback");


sig.properties и enc.properties идентичны и содержат следующее:

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=changeit
org.apache.ws.security.crypto.merlin.keystore.file=/home/grigory/out.jks       


Оказалось, что в формируемом клиентом SOAP запросе в заголовоке wsse:Security нет тега wsse:BinarySecurityToken. Я понял это только после того, как с помощью Glassfish Metro создал работающего клиента и сравнил отправляемые запросы.

Следующей на очереди была Glassfish Metro. Мне она изначально не нравилась из-за того, что заточена под использование в NetBeans, и из-за этого вся документация подается в виде "пойдите туда, нажмите эту кнопочку и все волшебным образом заработает". Но в итоге все-таки удалось создать решение, не зависящее от конкретной IDE, но, к сожалению, заставляющее программиста вручную редактировать xml, если в wsdl сервиса появятся изменения.

Итак, вот решение:
  • копируем wsdl и все зависимые wsdl и xsd файлы локально. В нашем случае это service.wsdl и wsdl0.wsdl. В wsdl0.wsdl описываются полиси для каждого метода, впоследствии мы их заменим.
  • дописываем к названию wsdl файлов .xml (и меняем внутри, если есть импорты)
  • создаем файл wsit-client.xml (он должен называться именно так) следующего содержания, в который просто включаем наш переименованный service.wsdl.xml
<?xml version="1.0" encoding="UTF-8"?>
<definitions
        xmlns="http://schemas.xmlsoap.org/wsdl/"
        xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
        name="mainclientconfig">
    <import location="service.wsdl.xml"
            namespace="http://testuri.com/wcflib/"/>
</definitions>

  • заменяем в файле wsdl0.wsdl.xml все wsp:Policy для всех методов на одну-единственную, следующего вида:
<wsdl:definitions 
...
                  xmlns:wsp1="http://www.w3.org/ns/ws-policy" 
...
/> 

<wsp1:Policy wsu:Id="WS2007HttpBinding_IWebService_policy">
        <wsp1:ExactlyOne>
            <wsp1:All>
                <sc:KeyStore wspp:visibility="private" alias="${keystore.private}"

                             storepass="${keystore.password}" type="JKS" 
                             location="${keystore.path}" keypass="${keystore.password}"/>
                <sc:TrustStore wspp:visibility="private" peeralias="${keystore.server}"

                             storepass="${keystore.password}" type="JKS"
                             location="${keystore.path}"/>
            </wsp1:All>
        </wsp1:ExactlyOne>
    </wsp1:Policy>


${keystore.path}, ${keystore.password} и т.п проставятся на этапе сборки.
WS2007HttpBinding_IWebService_policy нужно заменить на значение, прописанное в теге wsp:PolicyReference в wsdl:binding для вашего сервиса
  • Удаляем все ссылки на несуществующие полиси. Сами полиси мы удалили на предущем шаге, но ссылки в wsdl:input и wsdl:output на них остались.
  • Генерируем код клиента. Этот шаг мог быть первым, он не зависит от предыдущих. Для этого используем плагин для maven.
              <plugin>
                <groupId>org.jvnet.jax-ws-commons</groupId>
                <artifactId>jaxws-maven-plugin</artifactId>
                <version>2.1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>wsimport</goal>
                        </goals>
                        <configuration>
                            <extension>true</extension>
                            <bindingDirectory>
                                src/main/resources
                            </bindingDirectory>
                            <bindingFiles>
                                <bindingFile>jaxb-java-datatypes.xml</bindingFile>
                                <bindingFile>jaxb-binding-xsd4.xml</bindingFile>
                                <bindingFile>jaxb-binding-xsd9.xml</bindingFile>
                            </bindingFiles>
                            <wsdlDirectory>
                                src/main/resources
                            </wsdlDirectory>
                            <wsdlFiles>
                                <wsdlFile>service.wsdl</wsdlFile>
                            </wsdlFiles>
                            <wsdlLocation>http://testuri.com/wcflib-tc/service.svc</wsdlLocation>
                            <sourceDestDir>
                                ${basedir}/target/generated-sources
                            </sourceDestDir>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

Также очень полезным оказалось автоматическая генерация метода toString(). Этот метод генерируется другим плагином, поскольку я не нашел способа передать нужные мне параметры в XJC в jaxws-maven-plugin.
Ниже приведен сконфигурированный maven-jaxb2-plugin; указание forceRegenerate=true обязательно, иначе плагин ничего не будет делать
         <plugin>
                <groupId>org.jvnet.jaxb2.maven2</groupId>
                <artifactId>maven-jaxb2-plugin</artifactId>
                <version>0.7.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <generateDirectory>
                        ${basedir}/target/generated-sources
                    </generateDirectory>
                    <args>
                        <arg>-XtoString</arg>
                        <arg>-b</arg>
                        <arg> src/main/resources/jaxb-binding-xsd4.xml</arg>
                        <arg>-b</arg>
                        <arg> src/main/resources/jaxb-binding-xsd9.xml</arg>
                        <arg>-b</arg>
                        <arg> src/main/resources/jaxb-java-datatypes.xml</arg>
                    </args>
                    <plugins>
                        <plugin>
                            <groupId>org.jvnet.jaxb2_commons</groupId>
                            <artifactId>jaxb2-basics</artifactId>
                            <version>0.5.3</version>
                        </plugin>
                    </plugins>
                    <forceRegenerate>true</forceRegenerate>
                </configuration>
            </plugin>

  •  После того, как клиентские классы были сгенерированы, можно вызывать методы веб-сервиса. В нижеприведенном примере проставляются таймауты и указывается расположение wsdl.
     try{
            System.setProperty("sun.net.client.defaultConnectTimeout", "5000");
            System.setProperty("sun.net.client.defaultReadTimeout", "50000");
            URL baseUrl = TimeoutTest.class.getResource(".");
            String WEBSERVICE_WSDL_LOCATION =  "service.wsdl";
            GetCountriesRequestMessage r = new GetCountriesRequestMessage();
            WebService service = new WebService(new URL(baseUrl, WEBSERVICE_WSDL_LOCATION), new QName("http://testuri.com/wcflib/", "WebService"));
            IWebService iface = service.getWS2007HttpBindingIWebService();
            iface.getCountries(r);

        }catch (WebServiceException ex){
            if(ex.getCause() instanceof SocketTimeoutException){
                if(ex.getCause().getMessage().contains("connect timed out")){
                    System.out.println("Connection timeout");
                }else{
                    System.out.println("READ timeout! " + ex.getCause().getMessage());
                }
            }else if(ex.getCause() instanceof UnknownHostException) {
                System.out.println("No route to host! " + ex.getCause().getMessage());
            }else{
                ex.printStackTrace();
            }
        }catch (Exception ex){
            ex.printStackTrace();
        } 


В случае правильной конфигурации при запуске тестов в логе появится следующее:
Apr 30, 2013 2:53:59 PM [com.sun.xml.ws.policy.parser.PolicyConfigParser]  parse
INFO: WSP5018: Loaded WSIT configuration from file: file:/home/grigory/dev/metro/target/test-classes/wsit-client.xml.
Apr 30, 2013 2:54:01 PM com.sun.xml.ws.security.opt.impl.util.CertificateRetriever setServerCertInTheContext
INFO: WSS0824: The certificate found in the server wsdl or by server cert property is valid, so using it

Комментариев нет:

Отправить комментарий