Недавно мне потребовалось осуществить вызов веб-сервиса, защищенного 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
Итак, приступим. В принципе для конфигурирования можно было бы использовать 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