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