четверг, 20 ноября 2014 г.

Реализация взаимодействия с веб-сервисом и формирование гостовой подписи с помощью OpenSSL

В данной статье описывается как прикрутить OpenSSL для организации подписи (платежные шлюзы билайна и мосэнергосбыта). Подпись и проверку подписи будем делать вручную, перехватывая сообщения перед отправкой/после получения. поскольку реализация веб-сервиса не оставляет другого выбора.
Будем использовать apache CXF 2.7.8 для работы с веб-сервисом. Для подписи будем использовать OpenSSL, предварительно сконвертировав ключи в pkcs12 формат, как описано в предыдущей статье.

и немного кода:

        logger.info("---------------WS-URL---------------");
        logger.info(config.getString("ws.service.url"));
        logger.info("-------------------------------------");

        QName SERVICE_NAME = new QName("http://tempuri.org", "PaymentGateService");
        QName PORT_NAME = new QName("http://tempuri.org", "PaymentGatePort");

        Service paymnetService = Service.create(SERVICE_NAME);
        paymnetService.addPort(PORT_NAME, SOAPBinding.SOAP11HTTP_BINDING, Config.getWsAddress());
        PaymentGate iface = paymnetService.getPort(PORT_NAME, PaymentGate.class);

        SignatureInterceptor transformOutInterceptor = new SignatureInterceptor();
        Client client = ClientProxy.getClient(iface);
        HTTPConduit http = (HTTPConduit) client.getConduit();
        final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
            @Override
            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                return null;
            }
            @Override
            public void checkClientTrusted(X509Certificate[] certs, String authType) {
            }
            @Override
            public void checkServerTrusted(X509Certificate[] certs, String authType) {
            }
        }
        };
        TLSClientParameters parameters = new TLSClientParameters();
        parameters.setTrustManagers(trustAllCerts);
        parameters.setDisableCNCheck(true);
        http.setTlsClientParameters(parameters);

        HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy();

        httpClientPolicy.setConnectionTimeout(10000);
        httpClientPolicy.setAllowChunking(false);
        httpClientPolicy.setReceiveTimeout(40000);

        http.setClient(httpClientPolicy);

        client.getOutInterceptors().add(new LoggingOutInterceptor());
        client.getOutInterceptors().add(transformOutInterceptor);
        client.getInInterceptors().add(new SignatureVerificationInterseptor());
        System.setProperty("com.sun.xml.ws.transport.http.client.HttpTransportPipe.dump", String.valueOf(Config.isHttpDump()));

Для формирования подписи нам нужно получить сообщение целиком в том виде, в котором оно будет отправлено. Каноникализация производится неявным образом, путем удаления всего форматирования xml документа. используем следующий код, частично скопированный из исходников CXF. Из тонких моментов - инвертация полученной подписи. Видимо есть различия в том, как CryptoPro и OpenSSL возвращают результат подписи, кто-то в Big-endian, кто-то в Little-endian
public class SignatureInterceptor extends AbstractOutDatabindingInterceptor {
    private final static Logger logger = LoggerFactory.getLogger(SignatureInterceptor.class);

    public SignatureInterceptor() {
        super(Phase.PRE_LOGICAL);
        getBefore().add(HolderOutInterceptor.class.getName());
    }

    public void handleMessage(Message message) throws Fault {
        try {
            OutputStream out = new ByteArrayOutputStream();
            Exchange exchange = message.getExchange();
            Service service = exchange.getService();
            XMLOutputFactory instance = XMLOutputFactory.newInstance();
            XMLStreamWriter xmlWriter = instance.createXMLStreamWriter(out);

            DataWriter<XMLStreamWriter> dataWriter = getDataWriter(message, service, XMLStreamWriter.class);
            OperationInfo op = exchange.getBindingOperationInfo().getOperationInfo();
            List<MessagePartInfo> parts = op.getInput().getMessageParts();
            MessageContentsList objs = MessageContentsList.getContentsList(message);
            for (MessagePartInfo part : parts) {
                if (objs.hasValue(part)) {
                    Object o = objs.get(part);
                    if (!part.getConcreteName().getLocalPart().equals("digitalSignature")) {
                        dataWriter.write(o, part, xmlWriter);
                    }
                }
            }
            xmlWriter.flush();
            logger.debug("SIGNATURE BEGIN");
            logger.debug(out.toString());
            String signature = sign(out.toString());
            logger.debug(signature);
            logger.debug("signature length = " + signature.length());
            logger.debug("SIGNATURE END");
            xmlWriter.close();
            for (MessagePartInfo part : parts) {
                if (part.getConcreteName().getLocalPart().equals("digitalSignature")) {
                    DigitalSignature ds = new DigitalSignature();
                    ds.setSignature(signature);
                    objs.put(part, ds);
                }
            }
        } catch (Exception ex) {
            throw new Fault(ex);
        }
    }

    private static String sign(String content) throws SignatureException{
        File f = null;
        File fOut = null;
        try {
            f = File.createTempFile("input", "data");
            FileOutputStream foutStream = new FileOutputStream(f);
            IOUtils.write(content, foutStream);
            foutStream.flush();
            IOUtils.closeQuietly(foutStream);
            fOut = File.createTempFile("input", "sng");

            ProcessBuilder pb = new ProcessBuilder(Config.getInstance().getString("crypto.command"),
                    "dgst",
                    "-engine",
                    "gost",
                    "-binary",
                    "-md_gost94",
                    "-out",
                    fOut.getAbsolutePath(),
                    "-sign",
                    Config.getInstance().getString("crypto.private.key"),
                    f.getAbsolutePath());

            logger.debug(StringUtils.join(pb.command(), " "));

            Process p = pb.start();
            InputStream is = p.getErrorStream();
            p.waitFor();
            if (p.exitValue() != 0) {
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                IOUtils.copy(is, out);
                logger.error(out.toString("UTF-8"));
                IOUtils.closeQuietly(is);
                IOUtils.closeQuietly(out);
                throw new SignatureException("OpenSSL return error code: " + p.exitValue());
            }
            IOUtils.closeQuietly(is);
            FileInputStream fis = new FileInputStream(fOut);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            IOUtils.copy(fis, bos);
            IOUtils.closeQuietly(fis);
            byte[] res = bos.toByteArray();
            IOUtils.closeQuietly(bos);
            // Инвертируем полученную подпись - иначе она считается невалидной.
            byte[] inverted = new byte[res.length];
            for(int i = 0; i< res.length; i++){
                inverted[res.length-i-1] = res[i];
            }
            return new String(Base64.encodeBase64(inverted, false));
        } catch (IOException ex) {
            throw new SignatureException(ex.getMessage(), ex);
        } catch (InterruptedException e) {
            logger.error("Signature process was interrupted, returning null...");
            return null;
        } finally {
            FileUtils.deleteQuietly(f);
            FileUtils.deleteQuietly(fOut);
        }
    }


}
И проверка подписи:
public class SignatureVerificationInterseptor extends LoggingInInterceptor {
    private final static Logger logger = LoggerFactory.getLogger(SignatureVerificationInterseptor.class);

    public SignatureVerificationInterseptor() {
        super();
    }

    @Override
    protected String formatLoggingMessage(LoggingMessage loggingMessage) {
        String soapXmlPayload = loggingMessage.getPayload().toString();

        try {
            String content = soapXmlPayload.substring(soapXmlPayload.indexOf("<soapenv:Body>") + "<soapenv:Body>".length(), 
                                                   soapXmlPayload.indexOf("</soapenv:Body>"));
            String signature = soapXmlPayload.substring(soapXmlPayload.indexOf("<signature>") + "<signature>".length(),
                                                   soapXmlPayload.indexOf("</signature>"));
            logger.debug("signature verifying");
            logger.debug(content);
            logger.debug(signature);
            if (!verify(content, signature)) {
                logger.debug(super.formatLoggingMessage(loggingMessage));
                throw new SignatureVerificationException("Signature verification error");
            }
        }catch (SignatureVerificationException ex){
            throw ex;
        }catch (Exception ex){
            logger.debug(super.formatLoggingMessage(loggingMessage));
            throw new RuntimeException("Signature not found in soap envelope");
        }
        return super.formatLoggingMessage(loggingMessage);
    }

    private static boolean verify(String content, String signature){
        File signatureFile = null;
        File datafile = null;
        try {
            signatureFile = File.createTempFile("output", "sng");
            FileOutputStream signatureStream = new FileOutputStream(signatureFile);
            byte[] sig = Base64.decodeBase64(signature.getBytes());
            // Инвертируем полученную подпись - иначе она считается невалидной.
            byte[] inverted = new byte[sig.length];
            for(int i = 0; i< sig.length; i++){
                inverted[sig.length-i-1] = sig[i];
            }
            IOUtils.write(inverted, signatureStream);
            signatureStream.flush();
            IOUtils.closeQuietly(signatureStream);


            datafile = File.createTempFile("output", "data");
            FileOutputStream dataStream = new FileOutputStream(datafile);
            IOUtils.write(content, dataStream);
            dataStream.flush();
            IOUtils.closeQuietly(dataStream);

            ProcessBuilder pb = new ProcessBuilder(Config.getInstance().getString("crypto.command"),
                    "dgst",
                    "-engine",
                    "gost",
                    "-binary",
                    "-verify",
                    Config.getInstance().getString("crypto.public.cert"),
                    "-signature",
                    signatureFile.getAbsolutePath(),
                    datafile.getAbsolutePath());

            logger.debug(StringUtils.join(pb.command(), " "));

            Process p = pb.start();
            InputStream is = p.getErrorStream();
            p.waitFor();
            if (p.exitValue() != 0) {
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                IOUtils.copy(is, out);
                logger.error(out.toString("UTF-8"));
                IOUtils.closeQuietly(is);
                IOUtils.closeQuietly(out);
                return false;
            }
            return true;
        }catch (Exception ex){
            logger.error(ex.getClass().toString() + " message:" + ex.getMessage(), ex);
            return false;
        } finally {
            FileUtils.deleteQuietly(signatureFile);
            FileUtils.deleteQuietly(datafile);
        }
    }

вторник, 20 мая 2014 г.

Создание нескольких тегов ds:Signature в одном SOAP сообщении с использованием Apache CXF

Недавно мне потребовалось осуществить вызов веб-сервиса, защищенного WS-Security. Нужно было передать UsernameToken и подписать Body и UsernameToken двумя разными подписями. Решал попробовать решить эту задачу с помощью Apache CXF, в основном из-за того, что там не нужно вручную модифицировать wsdl и добавлять хитрые policy. Как оказалось, реализовать это в версии CXF 2.X невозможно без низкоуровневого манипулирования документом, что мне не очень-то хотелось делать. К счастью, наткнулся на этот архив http://osdir.com/ml/users-cxf-apache/2013-09/msg00285.html, откуда узнал, что недавно вышла версия CXF-3.0.0-milestone2 и там уже можно реализовать задуманное довольно просто.

Итак, приступим. В принципе для конфигурирования можно было бы использовать spring, но почти все настройки сделаны в коде
        // получаем интерфейс сервиса, который будем вызывать  
        PaymentService_Service service = new PaymentService_Service();
        PaymentService iface = service.getPaymentServiceImplPort();
        Client client = ClientProxy.getClient(iface);
        HTTPConduit http = (HTTPConduit) client.getConduit();
        TLSClientParameters parameters = new TLSClientParameters();

        // задаем keyStore и trustStore для работы по https с клиентским сертификатом
        parameters.setSSLSocketFactory(createSSLContext().getSocketFactory());
        http.setTlsClientParameters(parameters);

        HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy();

        httpClientPolicy.setConnectionTimeout(10000);
        httpClientPolicy.setAllowChunking(false);
        httpClientPolicy.setReceiveTimeout(30000);

        http.setClient(httpClientPolicy);


   public void testWebMethod() throws Exception {
        List<HandlerAction> actions = new ArrayList<HandlerAction>();
        {

            // говорим, что мы хотим UsernameToken
            HandlerAction action = new HandlerAction();
            action.setAction(WSConstants.UT);
            actions.add(action);
        }

        // говорим, что мы хотим отдельную подпись для Body
        {

            // определим объект(ы) которые будем подписывать
            // сначала локальное имя тега (без namespace), затем namespace, потом одно из значений Content или Element
            WSEncryptionPart part = new WSEncryptionPart(WSConstants.ELEM_BODY, "http://schemas.xmlsoap.org/soap/envelope/", "Content" );
            List<WSEncryptionPart> parts = new ArrayList<WSEncryptionPart>();
            parts.add(part);
            addSignatureAction(actions, parts);
        }

        // говорим, что мы хотим отдельную подпись для UsernameToken
        {
            WSEncryptionPart part = new WSEncryptionPart("UsernameToken", WSConstants.WSSE_NS, "Content" );
            List<WSEncryptionPart> parts = new ArrayList<WSEncryptionPart>();
            parts.add(part);
            addSignatureAction(actions, parts);
        }

        Client client = ClientProxy.getClient(iface);
        client.getInInterceptors().add(new LoggingInInterceptor());
        client.getOutInterceptors().add(new LoggingOutInterceptor());

        Map<String,Object> inProps= new HashMap<String,Object>();
        // указываем наш список действий
        inProps.put(WSHandlerConstants.HANDLER_ACTIONS, actions);

        // пароль простым текстом (он у нас всегда пустой)
        inProps.put(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_TEXT);

        // Колбэк для задания паролей от keystore для подписи и UsernameToken
        inProps.put(WSHandlerConstants.PW_CALLBACK_CLASS, PasswordCallbackHandler.class.getName());

        // пользователь, который будет указан в UsernameToken
        inProps.put(WSHandlerConstants.USER, "00000000");

        client.getOutInterceptors().add(new WSS4JOutInterceptor(inProps));
        String result = iface.doAnyMethodOfService();

    }

  
    Поскольку используется не финальная версия, есть некоторые недоработки, в частности, нельзя задать некоторые свойства по-старому, нужно в обязательном порядке перечислить их в объекте token.
    private void addSignatureAction(List<HandlerAction> actions, List<WSEncryptionPart> parts) throws WSSecurityException {
        HandlerAction action = new HandlerAction();
        SignatureActionToken token = new SignatureActionToken();
        token.setIncludeSignatureToken(true);
        token.setParts(parts);

        // файл с описанием криптопровайдера
        token.setCryptoProperties("sig.properties");

        // алиас приватного ключа. Пароль задается в классе PasswordCallbackHandler
        token.setUser("1");

        // будет ли включен в тег BinarySecurityToken один сертификат или вся цепочка
        token.setUseSingleCert(true);

        // каким образом информация о сертификате будет представлена в структуре ds:KeyInfo
        // в данном случае ссылкой на тег BinarySecurityToken, в которой лежит весь сертификат
        token.setKeyIdentifierId(WSConstants.BST_DIRECT_REFERENCE);
        token.getCrypto(); // set up fields
        action.setAction(WSConstants.SIGN);
        action.setActionToken(token);
        actions.add(action);
    }


Класс для задания паролей к хранилищу и токену
public class PasswordCallbackHandler implements CallbackHandler {
    @Override
    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
        WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];
        if (WSPasswordCallback.USERNAME_TOKEN == pc.getUsage()) {
            pc.setPassword("");
        }
        if (WSPasswordCallback.SIGNATURE == pc.getUsage()) {

            pc.setPassword("changeit");
        }
    }
}


Кочующая из проекта в проект c небольшими вариациями функция по получению SSLContext. В данном случае используется два разных keystore, один для хранения приватного ключа и сертификата, второй для хранения цепочки сертификатов сервера.

private SSLContext createSSLContext() throws Exception{
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        trustStore.load(new FileInputStream("/home/grigory/dev/project/src/test/key/https.jks"), "changeit".toCharArray());
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(new FileInputStream("/home/grigory/dev/project/src/test/key/client.jks"), "changeit".toCharArray());
        TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
        tmf.init(trustStore);
        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
        kmf.init(keyStore, "changeit".toCharArray());
        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(kmf.getKeyManagers() , tmf.getTrustManagers(), new SecureRandom());
        return sslContext;

    }


И для примера содержимое файла sig.properites (должен лежать в classpath)
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/dev/project/src/test/key/sig.jks




четверг, 6 марта 2014 г.

Формирование и проверка УИН ГИБДД

С февраля 2014 в России вводится новый Универсальный Идентификатор начислений (УИН) http://savepayment.ru/soft/53/uin-v-platezhnom-poruchenii. Это 20-ти значная строка, которая содержит цифры и русские и латинские буквы. И если ранее штраф идентифицировался по номеру и дате платежного постановления, то теперь будут УИНы. В принципе, можно осуществить конвертацию УИН в номер дату платежного постановления и обратно. Алгоритм формирования УИН для ГИБДД описан тут (осторожно, множество опечаток в тестовых данных): Письмо МВД России от 9 августа 2013 г. № 13/9-4902 "Об информировании кредитных организаций о правилах формирования уникального идентификатора начислений" 
UPDATE: с какого-то момента 2014 года опять все поменялось, и УИН ГИБДД теперь содержит только цифры, без букв, и в нем уже не закодирован номер и дата постановления. http://savepayment.ru/recommendation/51/kvitantsiya-gibdd-izmenilas  из такого УИНа нельзя достать номер и дату постановления!
Непосредственно реализация конвертации УИН старого образца в пару номер/дата постановления

public class UIN {
    private static final char[] map =   {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
            'А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ж', 'З', 'И', 'К', 'Л', 'М', 'Н', 'О', 'П', 'Р', 'С', 'Т', 'У', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Ь', 'Э', 'Ю', 'Я'};


    private static final int[] weights = { 0,  1,   2,   3,   4,   5,   6,   7,   8,   9,
            1,   3,   17,  29,   6,   30,  31, 13,  32,  33,  10,  34,  12,  35,  14,  16,  36,  37,  38,  18,  39,  40,  41,  21,  19,  42,
            1,   2,   3,   4,    5,   6,   7,  8,   9,   10,  11,  12,  13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  42,  26,  27,  28 };


    public static boolean checkCRC(String uin){
        if(uin.length() != 20){
            throw new RuntimeException("incorrect uin length");
        }
        String finalNumber = uin.substring(0, 19);
        int crc = calculateCRC(finalNumber, 1);
        if(crc % 11 == 10){
            crc = calculateCRC(finalNumber, 3);
            if(crc %11 == 10){
                crc = 0;
            }
        }
        crc = crc%11;
        return uin.charAt(19) == Character.forDigit(crc, 10);
    }

    public static String getFineNumber(String uin){
        if(uin.length() != 20){
            throw new RuntimeException("incorrect uin length");
        }
        return uin.substring(7,19).replaceAll("(Z){1,12}$", "");
    }


    public static Date getFineDate(String uin){
        if(uin.length() != 20){
            throw new RuntimeException("incorrect uin length");
        }
        int res = 0;
        String dateStr = uin.substring(5,7);
        for (int i = dateStr.length(); i > 0; i--) {
            int index = lookupIndex(dateStr.charAt(dateStr.length() - i));
            res += index * Math.pow(64, i - 1);
        }
        Calendar c = Calendar.getInstance();
        int year = c.get(Calendar.YEAR);
        if (year % 10 == 0 && res % 10 != 0) { // в 2020 году могут оплатить штраф за 2019-й
            c.set(Calendar.YEAR, ((year / 10) - 1) * 10 + res % 10);
        } else {
            c.set(Calendar.YEAR, (year / 10) * 10 + res % 10);
        }
        c.set(Calendar.DAY_OF_YEAR, res / 10); // важно сначала установить год, а потом день года
        return c.getTime();
    }

    private static int lookupIndex(char ch) {
        for (int i = 0; i < map.length; i++) {
            if (map[i] == ch) {
                return i;
            }
        }
        throw new RuntimeException("Invalid character: '" + ch + "'");
    }

    private static int calculateCRC(String finalNumber, int startIndex) {
        int crc = 0;
        int i = startIndex;
        for(char ch : finalNumber.toCharArray()){
            crc += i++ * (weights[lookupIndex(ch)]%10);
            if(i==11){
                i=1;
            }
        }
        return crc;
    }
}

юнит-тест, использующий исправленные входные данные отсюда: http://www.garant.ru/products/ipo/prime/doc/70339846

@Test
    public void testUIN() throws Exception {
        SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy");

        Assert.assertTrue(UIN.checkCRC("18810R564BK229683ZZ8"));
        Assert.assertTrue(UIN.checkCRC("18810ЗД57KT002522ZZ2"));
        Assert.assertTrue(UIN.checkCRC("18810HФ50АО309188ZZ5"));

        Assert.assertEquals("64BK229683", UIN.getFineNumber("18810R564BK229683ZZ8"));
        Assert.assertEquals("57KT002522", UIN.getFineNumber("18810ЗД57KT002522ZZ2"));
        Assert.assertEquals("50АО309188", UIN.getFineNumber("18810HФ50АО309188ZZ5"));

        Assert.assertEquals("22.06.2013", sdf.format(UIN.getFineDate("18810R564BK229683ZZ8")));
        Assert.assertEquals("24.04.2013", sdf.format(UIN.getFineDate("18810HФ50АО309188ZZ5")));
        Assert.assertEquals("05.10.2012", sdf.format(UIN.getFineDate("18810ЗД57KT002522ZZ2")));
    }


и небольшой юнит-тест, который использует те-же функции, для обратной конвертации. Он не оформлен как класс и написан непонятно зачем:
    @Test
    public void testPackUinDate() throws Exception {
        String number ="64BK229683";
        String dateStr = "22.06.2013";
        Date d = new SimpleDateFormat("dd.MM.yyyy").parse(dateStr);

        Calendar c = Calendar.getInstance();
        c.setTime(d);

        int tmp = (c.get(Calendar.DAY_OF_YEAR)) * 10 + (c.get(Calendar.YEAR) % 10);
        String result = "";
        while (tmp > 64) {
            result += map[tmp % 64];
            tmp = tmp / 64;
        }
        result += map[tmp];
        Assert.assertEquals("R5", StringUtils.reverse(result));
        String finalNumber = "18810" +  StringUtils.reverse(result) +  StringUtils.rightPad(number, 12, 'Z');
        Assert.assertEquals("18810R564BK229683ZZ", finalNumber);

        int crc = calculateCRC(finalNumber, 1);
        if(crc % 11 == 10){
            crc = calculateCRC(finalNumber, 3);
            if(crc %11 == 10){
                crc = 0;
            }
        }
        crc = crc%11;
        Assert.assertEquals(8, crc);
        Assert.assertEquals("18810R564BK229683ZZ8", finalNumber+crc);

    }

понедельник, 2 сентября 2013 г.

Добавление спрингового HttpRequestHandlerServlet сервлета в Tapestry приложение

Недавно возникла небольшая проблема: как добавить свой спринговый сервлет в Tapestry приложение. Если просто прописать его в web.xml, например так:
    <servlet>
        <servlet-name>PaymentService</servlet-name>
        <servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>PaymentService</servlet-name>
        <url-pattern>/PaymentService</url-pattern>
    </servlet-mapping>


то Tapestry filter перехватывает запрос к этому сервлету, из-за вот такой настройки:
    <filter>
        <filter-name>app</filter-name>
        <filter-class>org.apache.tapestry5.TapestryFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>app</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


Как исключить что-то в url-pattern, я так и не понял, но, к счастью, тапестри позволяет настроить обработку HttpRequest и вручную определить, какие фильтры к нему применять. Нужно всего лишь добавить следующий код в AppModule
       @Contribute(HttpServletRequestHandler.class)
    public void сontributeFilter(OrderedConfiguration<HttpServletRequestFilter> config){
        config.add("httpInvoker", new HttpServletRequestFilter() {
            @Override
            public boolean service(HttpServletRequest request, HttpServletResponse response, HttpServletRequestHandler handler) throws IOException {
                if(request.getServletPath().equals("/PaymentService")){ // имя нашего сервлета
                    try{
                        AppContext.getHttpInvoker(StringUtils.removeStart(request.getServletPath(), "/")).handleRequest(request, response);
                    }catch (ServletException ex){
                        throw new IOException(ex);
                    }
                    return true; // мы обработали запрос сами, не нужно его обрабатывать другими фильтрами.
                }
                return handler.service(request, response); // передаем запрос тапестри
            }
        });
    }


AppContext.getHttpInvoker - это хелпер, который возвращает spring bean по его имени, приведя его к нужному типу.
    public static HttpInvokerServiceExporter getHttpInvoker(String name){
        if(context == null){
            throw new ApplicationNotInitializedException();
        }
        return (HttpInvokerServiceExporter) context.getBean(name);
    }

 
И, для полноты картины, кусочек конфига спринга:
    <bean name="PaymentService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
        <property name="service" ref="PaymentServiceImpl"/>
        <property name="serviceInterface" value="com.example.httpInvoker.PaymentService"/>
    </bean>


пятница, 7 июня 2013 г.

Изменение стилей Tapestry приложения

В данной статье описывается, как поменять стили у программы, написанной с использованием Tapestry 5.3.x

Добавляем нашу любимую библиотечку со стилями, например bootstrap.css. Это почти сразу придаст приложению определенный общий вид, как будто для него разрабатывали дизайн. Но к сожалению, некоторые вещи, в частности валидация, пейджинг, таблицы, начинают смотреться чужеродно. Например вот так выглядит страница логина после применения bootstrap.css
К сожалению, исправить там что-то уже довольно сложно.
Поэтому принимаем решение полностью отказаться от стилей tapestry в приложении. Делается это следующим образом: добавляем к AppModule.java вот такой код
    @Contribute(MarkupRenderer.class)
    public static void deactiveDefaultCSS(OrderedConfiguration<MarkupRendererFilter> configuration)
    {
        configuration.override("InjectDefaultStylesheet", null);
    }
  

После удаления таблицы стилей страницы выглядят пустовато; но ничего, скоро мы это исправим.
Добавляем style.css - нашу собственную таблицу стилей. В ней мы переопределим некоторые стили tapestry.

Для начала, добавим стили для корректного отображения ошибок:

#корректируем ошибку tapestry с заползанием текста ошибки за левый край
DIV.t-error LI{
  margin-left: 20px;
}

#задаем цвет попапа с ошибкой
DIV.t-error-popup{
   color: #b94a48;
}

#рисуем стрелку вниз
.t-error-popup > span:after {
    border-color: #fcf8e3 transparent;
    border-style: solid;
    border-width: 15px 15px 0;
    bottom: 0px;
    content: "";
    display: block;
    left: 20px;
    position: absolute;
    width: 0;
}

#рисуем попап
DIV.t-error-popup > span {
   padding: 4px;
   text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
   border-color: #eed3d7;
   background-color: #fcf8e3;
   border: 1px solid #eed3d7;
   -webkit-border-radius: 4px;
      -moz-border-radius: 4px;
           border-radius: 4px;
}

#Задаем цвет контура поля ввода с ошибкой
input.t-error{
      border-color: #eed3d7;
}


И стили для корректного отображения pager'а

#стили для grid pager, скопированы из bootstrap.css
DIV.t-data-grid-pager{
    padding-left: 15px;
    display: inline-block;
  *display: inline;
  margin-bottom: 4px;
  margin-left: 0;
  -webkit-border-radius: 4px;
     -moz-border-radius: 4px;
          border-radius: 4px;
  *zoom: 1;
}
.t-data-grid-pager > a,
.t-data-grid-pager > span {
  padding: 4px 12px;
  line-height: 20px;
  text-decoration: none;
  background-color: #ffffff;
  border: 1px solid #dddddd;
  /*border-left-width: 0;*/
}
.t-data-grid-pager > a:hover,
.t-data-grid-pager > a:focus,
.t-data-grid-pager > .current > a,
.t-data-grid-pager > .current > span {
  background-color: #f5f5f5;
}
.t-data-grid-pager > .current > a,
.t-data-grid-pager > .current > span {
  color: #999999;
  cursor: default;
}
.t-data-grid-pager a:first-child,
.t-data-grid-pager span:first-child {
  border-left-width: 1px;
  -webkit-border-bottom-left-radius: 4px;
          border-bottom-left-radius: 4px;
  -webkit-border-top-left-radius: 4px;
          border-top-left-radius: 4px;
  -moz-border-radius-bottomleft: 4px;
  -moz-border-radius-topleft: 4px;
}
.t-data-grid-pager a:last-child,
.t-data-grid-pager span:last-child  {
  -webkit-border-top-right-radius: 4px;
          border-top-right-radius: 4px;
  -webkit-border-bottom-right-radius: 4px;
          border-bottom-right-radius: 4px;
  -moz-border-radius-topright: 4px;
  -moz-border-radius-bottomright: 4px;
}

Результат:



Подправим beandisplay

DL.t-beandisplay {
    background: none repeat scroll 0 0 rgb(245, 245, 245);
    display: block;
    padding: 2px;
    width: auto;
}
DL.t-beandisplay DT:after {
    content: ":";
}
DL.t-beandisplay DT {
    clear: left;
    display: inline;
    float: left;
    padding-right: 3px;
    text-align: right;
    vertical-align: middle;
    width: 250px;
}


Поправим показ таблиц (выделим заголовок)
.table th {
  white-space: nowrap;
  line-height: 20px;
  background-color: rgb(245, 245, 245);
}


Осталось немного допилить тексты сообщений, выводимые Tapestry при срабатывании валидации. Чтобы их скастомизировать, необходимо создать следующие файлы:
org.apache.tapestry5.corelib.components.Errors.properties следующего содержания:
default-banner=Вам необходимо исправить следующее, чтобы продолжить:
И org.apache.tapestry5.internal.ValidationMessages.properties в котором можно переопределить сообщения, выдаваемые при различных видах валидации, например для required:
required=Укажите значение

Результат:



четверг, 6 июня 2013 г.

Организация security в приложениях, написанных с использованием Tapesrty5

Существует несколько различных способов обеспечения безопасности в приложениях на Tapestry 5. В данной статье описывается только один из них - security с использованием библиотеки shiro и прослойки, которая осуществляет интеграцию с этой библиотекой - tapestry-security

При использовании maven необходимо добавить в зависимости:

        <dependency>
            <groupId>org.tynamo</groupId>
            <artifactId>tapestry-security</artifactId>
            <version>0.4.6</version>
        </dependency>


Самый простой случай, когда пользователи и пароли хранятся в файле, рассматривать не будем, это сделано в документации, вместо этого создадим свой собственный Realm. Ограничимся разделением доступа по ролям, с предопределенным набором ролей.
Для начала создадим класс пользователя User, экземпляр этого класса будет использоваться как SecurityPrincipal в дальнейшем.

import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.util.ByteSource;
import javax.persistence.*;


@Entity
@Table(name = "TEST_USER")
public class User implements Serializable{
    public static final String ROLE_ADMIN = "admin", ROLE_OPERATOR = "operator";
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true, nullable = false, length = 100)
    private String login;
    @Column(nullable = false)
    private String name;
    @Column(nullable = true)
    private String passwordHash;
    @Column(nullable = false)
    private boolean enabled = true;
    @Column(length = 128)
    private byte[] salt;
    @Column(nullable = false)
    private boolean admin = false;
   @Override
    public String toString() {
        return name;
    }
    public void setPassword(String password){
        ByteSource saltSource = new SecureRandomNumberGenerator().nextBytes();
        this.setSalt(saltSource.getBytes());
        this.setPasswordHash(new
Sha256Hash(password, saltSource).toString());
    }
}


Тривиальные get/set методы опущены. Как можно заметить, пароль не хранится в открытом виде, хранится только SHA-256 хэш пароля и соль.

Создадим свой собственный Realm для авторизации и аутентификации пользователей.

import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.SimpleByteSource;
import org.example.test.AppContext;
import
org.example.test.domain.User;

import java.util.HashSet;
import java.util.Set;

public class UserRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        if(principalCollection.isEmpty()) return null;
        try{
            User user = (User)principalCollection.fromRealm(getName()).iterator().next();
            if (user == null) return null;
            Set<String> roles = new HashSet<String>();

            // здесь используются только две роли, но ничто не мешает пользователю иметь несколько ролей одновременно
            if(user.isAdmin()){
                roles.add(User.ROLE_ADMIN);
            }else{
                roles.add(User.ROLE_OPERATOR);
            }
            return new SimpleAuthorizationInfo(roles);
        }catch (Exception ex){
            ex.printStackTrace();
            return null;
        }
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        // Null username is invalid
        if (username == null) { throw new AuthenticationException("Void username are not allowed by this realm."); }

        // запрос на получение пользователя из базы данных
        User user = AppContext.getUserDao().findByLogin(username);
        if(user == null){
            throw new UnknownAccountException("user not found");
        }
        return new SimpleAuthenticationInfo(user, user.getPasswordHash(), new SimpleByteSource(user.getSalt()), getName());
    }

    public UserRealm() {
        super(new MemoryConstrainedCacheManager());
        setName("dbaccounts");
        setAuthenticationTokenClass(UsernamePasswordToken.class);
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("SHA-256");
        setCredentialsMatcher(matcher);
    }
}


После этого можно приступать к настройке доступа к страницам и методам. Предположим, что все, что доступно админу находится в директории admin, а все, что доступно оператору - в директории operator. Страницы логина, запроса на смену пароля и ошибок находятся в корне и доступ к ним возможен для неавторизованных пользователей.

Пропишем эти настройки в AppModule.java Очень похоже на черную магию, и собственно это так и есть. Как и почему это работает, я не понимаю. Возможно это работает из-за аннотаций, или потому что переданы определенные параметры. или указано определенное имя функции, кто знает...

import org.apache.shiro.realm.Realm;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.ioc.MappedConfiguration;
import org.apache.tapestry5.ioc.annotations.Contribute;
import org.apache.tapestry5.ioc.annotations.Marker;
import org.apache.tapestry5.ioc.services.ApplicationDefaults;
import org.apache.tapestry5.ioc.services.SymbolProvider;
import org.apache.tapestry5.services.HttpServletRequestFilter;
import org.tynamo.security.Security;
import org.tynamo.security.SecuritySymbols;
import org.tynamo.security.services.SecurityFilterChainFactory;
import org.tynamo.security.services.impl.SecurityFilterChain;
import org.example.test.domain.User;


    @Contribute(WebSecurityManager.class)
    public static void addRealms(Configuration<Realm> configuration) {
        UserRealm ur = new UserRealm();
        configuration.add(ur);
    }

    @Contribute(SymbolProvider.class)
    @ApplicationDefaults
    public static void applicationDefaults(MappedConfiguration<String, Object> configuration){
        // Tynamo's tapestry-security (Shiro) module configuration
        configuration.add(SecuritySymbols.LOGIN_URL, "/login");
        configuration.add(SecuritySymbols.SUCCESS_URL, "/index");
        configuration.add(SecuritySymbols.UNAUTHORIZED_URL, "/accessDenied");
    }

    @Contribute(HttpServletRequestFilter.class)
    @Marker(Security.class)
    public static void contributeSecurityConfiguration(Configuration<SecurityFilterChain> configuration,
                                                       SecurityFilterChainFactory factory,
                                                       WebSecurityManager securityManager) {
        configuration.add(factory.createChain("/admin/**").add(factory.roles(), User.ROLE_ADMIN).build());
        configuration.add(factory.createChain("/operator/**").add(factory.roles(), User.ROLE_OPERATOR).build());
        configuration.add(factory.createChain("/css/**").add(factory.anon()).build());
        configuration.add(factory.createChain("/*").add(factory.anon()).build());
    }


После того, как все настройки прописаны, займемся непосредственно страничками.
Для начала настроим страницу логина, поскольку то, что идет по умолчанию с tapestry-security, не всегда вписывается в общий дизайн приложения.

import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.StringUtils;
import org.apache.shiro.web.util.SavedRequest;
import org.apache.shiro.web.util.WebUtils;
import org.apache.tapestry5.PersistenceConstants;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.RequestGlobals;
import org.apache.tapestry5.services.Response;
import org.tynamo.security.services.PageService;
import org.tynamo.security.services.SecurityService;
import java.io.IOException;

public class Login {

    @Property
    private String jsecLogin;
    @Property
    private String jsecPassword;
    @Property
    private boolean jsecRememberMe;
    @Persist(PersistenceConstants.FLASH)
    private String loginMessage;
    @Inject
    private Response response;
    @Inject
    private RequestGlobals requestGlobals;
    @Inject
    private SecurityService securityService;
    @Inject
    private PageService pageService;

    public Object onActionFromJsecLoginForm() {
        Subject currentUser = securityService.getSubject();
        if (currentUser == null) {
            throw new IllegalStateException("Subject cannot be null");
        }
        UsernamePasswordToken token = new UsernamePasswordToken(jsecLogin, jsecPassword);
        token.setRememberMe(jsecRememberMe);
        try {
            currentUser.login(token);
        } catch (UnknownAccountException e) {
            loginMessage = "Такого пользователя нет в базе";
            return null;
        } catch (IncorrectCredentialsException e) {
            loginMessage = "Неверный пароль";
            return null;
        } catch (LockedAccountException e) {
            loginMessage = "Пользователь заблокирован";
            return null;
        } catch (AuthenticationException e) {
            loginMessage = "Ошибка авторизации";
            return null;
        }
        SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(requestGlobals.getHTTPServletRequest());
        if (savedRequest != null) {
            try {
                WebUtils.redirectToSavedRequest(requestGlobals.getHTTPServletRequest(), requestGlobals.getHTTPServletResponse(), pageService.getSuccessPage());
                return savedRequest.getRequestURI();
            } catch (IOException e) {
                return pageService.getSuccessPage();
            }
        } else {
            return pageService.getSuccessPage();
        }
    }
    public String getLoginMessage() {
        if (hasLoginMessage()) {
            return loginMessage;
        } else {
            return " ";
        }
    }
    public boolean hasLoginMessage() {
        return StringUtils.hasText(loginMessage);
    }
}


Login.tml В файле layout'а прописаны ссылки на bootstrap.css, и не используется css от tapestry. Как сделать красивые странички, и сколько проблем при этом возникло - тема для отдельного поста.

<t:layout title="literal:Вход" bodyId="signin"
          xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
          xmlns:p="tapestry:parameter">
    <t:form class="form-horizontal" t:id="jsecLoginForm">
        <t:if test="hasLoginMessage()">
        <div class="control-group">
            <div class="controls">
            <label class="text-error">${loginMessage}</label>
            </div>
        </div>
        </t:if>
        <div class="control-group">
            <label class="control-label" for="jsecLogin">Логин</label>
            <div class="controls">
                <t:textfield t:id="jsecLogin" class="input-large" validate="required" size="40" tabindex="1"  placeholder="Login"/>
            </div>
        </div>
        <div class="control-group">
            <label class="control-label" for="jsecPassword">Пароль</label>
            <div class="controls">
                <t:passwordfield t:id="jsecPassword" class="input-large" validate="required" size="40" tabindex="2" placeholder="Password" />
            </div>
        </div>
        <div class="control-group">
            <div class="controls">
                <t:pageLink page="getPass">Забыли пароль?</t:pageLink>
                <label class="checkbox">
                    <t:checkbox t:id="jsecRememberMe" tabindex="3" /> Запомнить меня
                </label>
                <input type="submit" name="login" class="btn" value="Войти" tabindex="4" />
            </div>
        </div>
    </t:form>
</t:layout>


После успешного логина пользователя пребрасывает на index, где уже осуществляется переброс в засимости от роли.

public class Index {
    @InjectPage
    private AdminIndex adminIndex;
    @InjectPage
    private OperatorIndex operatorIndex;
    @OnEvent(value = EventConstants.ACTIVATE)
    public Object initPage() {
        if(SecurityUtils.getSubject().hasRole(User.ROLE_ADMIN)){
            return adminIndex;
        }else{
            return operatorIndex;
        }
    }
}


И остался небольшой момент - это logout. Функция logout реализована непосредственно в шаблоне

public class Layout {

    @InjectPage
    private Index index;
    @Inject
    @Property
    private HttpServletRequest request;
    public String getUsername() {
        try {

            return SecurityUtils.getSubject().getPrincipal().toString();
        } catch (NullPointerException ex) {
            return "guest";
        }
    }

    public Object onActionFromLogout() {
        SecurityUtils.getSecurityManager().logout(SecurityUtils.getSubject());
        try {
            final HttpSession session = this.request.getSession(false);
            if (session != null) {
                session.invalidate();
            }
        } catch (final IllegalStateException ex) { }
        return index;
    }
}


И Layout.tml

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
      xmlns:p="tapestry:parameter" >
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <link href="${request.contextPath}/css/bootstrap.css" rel="stylesheet" type="text/css"/>
    <link href="${request.contextPath}/css/mystyle.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<t:security.user>
    <p class="pad-left">Здравствуйте, ${username}

    <t:actionLink t:id="logout" class="pull-right pad-right"> Выйти</t:actionLink></p>
</t:security.user>
<div class="control-group">
    <div class="control">
        <t:body/>
    </div>
</div>
</body>
</html>


В общем-то этого должно быть достаточно. Если необходимо получить где-то в коде текущего залогиненного пользователя, достаточно выполнить (org.example.test.domain.User) SecurityUtils.getSubject().getPrincipal();

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