вторник, 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

четверг, 11 апреля 2013 г.

wsdl2java: Two declarations cause a collision in the ObjectFactory class.

1. Получил странную ошибку при генерации: Two declarations cause a collision in the ObjectFactory class.
2. Непонятно где ошибка, поскольку xsd без перевода строки
3. Скачал локально, отформатировал, переписал все импорты в xsd и wsdl
4. Проблемных мест было два, в первой xsd:

первое объявление
<xs:element name="PersonAddress" nillable="true" type="tns:PersonAddress"/>

второе объявление
<xs:complexType name="Person">
        <xs:sequence>
            <xs:element minOccurs="0" name="Address" nillable="true" type="tns:PersonAddress"/>
   skip

во второй:

<xs:element name="BankFlags" nillable="true" type="tns:BankFlags"/>
<xs:complexType name="Bank">
        <xs:sequence>
            <xs:element minOccurs="0" name="Flags" nillable="true" type="tns:BankFlags"/>


skip

т.е ObjectFactory генерируется не совсем корректно для подобных случаев.

5. Решение нашлось тут: http://stackoverflow.com/questions/13422253/xjc-two-declarations-cause-a-collision-in-the-objectfactory-class

6. Создал фалы биндингов для этих случаев:
jaxb-binding-xsd4.xml

<jaxb:bindings xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
               xmlns:xs="http://www.w3.org/2001/XMLSchema"
               version="1.0">
    <jaxb:bindings schemaLocation="xsd4.xsd">
        <jaxb:bindings node="//xs:element[@name='PersonAddress']">
            <jaxb:factoryMethod name="TypePersonAddress"/>
        </jaxb:bindings>
    </jaxb:bindings>
</jaxb:bindings>

и
jaxb-binding-xsd9.xml

<jaxb:bindings xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
               xmlns:xs="http://www.w3.org/2001/XMLSchema"
               version="1.0">
    <jaxb:bindings schemaLocation="xsd9.xsd">
        <jaxb:bindings node="//xs:element[@name='BankFlags']">
            <jaxb:factoryMethod name="TypeBankFlags"/>
        </jaxb:bindings>
    </jaxb:bindings>
</jaxb:bindings>

7. Прописываем параметры в pom.xml

<plugin>
                <groupId>org.apache.cxf</groupId>
                <artifactId>cxf-codegen-plugin</artifactId>
                <version>2.7.3</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>wsdl2java</goal>
                        </goals>
                        <configuration>
                            <sourceRoot>${basedir}/target/generated-sources</sourceRoot>
                            <wsdlOptions>
                                <wsdlOption>
                                    <extraargs>
                                        <extraarg>-verbose</extraarg>                                        <extraarg>-b</extraarg>
                                        <extraarg>${basedir}/src/main/resources/jaxb-binding-xsd4.xml</extraarg>
                                        <extraarg>-b</extraarg>
                                        <extraarg>${basedir}/src/main/resources/jaxb-binding-xsd9.xml</extraarg>
                                    </extraargs>
                                    <wsdl>
                                        ${basedir}/src/main/resources/service.wsdl
                                    </wsdl>
                                    <wsdlLocation>classpath:service.wsdl</wsdlLocation>
                                </wsdlOption>
                            </wsdlOptions>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
8. Классы сгенерировались, ура!