Задача
Есть приложение, которое может запрашивать данные по определённому протоколу и есть сервис, который может отдавать данные, но по другому протоколу и требует авторизации по логину и паролю.
В приложении нет учётных данных необходимых для доступа к сервису. Необходимо подружить приложение и сервис.
Решение
Использовать шину: WSO2 Enterprise Service Bus, на которой настроить прокси-сервис.
Используем WSO2 ESB версии 4.7.0.
Создаём политики
Так как целевой сервис защищён по стандарту WS-Security, то и доступ к нему будем настраивать по всей строгости стандарта.
Политики создаются как ресурсы в локальном репозитории, в интерфейсе: Manage — Service Bus — Local Entries — Add Local Entries — Add In-lined XML Entry.
Политика для исходящих сообщений
Здесь и в дальнейшем имена могут даваться произвольные, но буду акцентировать на них внимание, потому что через них будут связываться все артефакты в одно целое
Указываем имя: service-policy
Указываем политику:
<wsp:Policy xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="UTOverTransport"> <wsp:ExactlyOne> <wsp:All> <sp:TransportBinding xmlns:sp="http://schemas.xmlsoap.org/ws/2005/07/securitypolicy"> <wsp:Policy> <sp:TransportToken> <wsp:Policy> <sp:HttpsToken RequireClientCertificate="false"/> </wsp:Policy> </sp:TransportToken> <sp:AlgorithmSuite> <wsp:Policy> <sp:Basic128/> </wsp:Policy> </sp:AlgorithmSuite> <sp:Layout> <wsp:Policy> <sp:Lax/> </wsp:Policy> </sp:Layout> <sp:IncludeTimestamp/> </wsp:Policy> </sp:TransportBinding> <sp:SignedSupportingTokens xmlns:sp="http://schemas.xmlsoap.org/ws/2005/07/securitypolicy"> <wsp:Policy> <sp:UsernameToken sp:IncludeToken="http://schemas.xmlsoap.org/ws/2005/07/securitypolicy/IncludeToken/AlwaysToRecipient"/> </wsp:Policy> </sp:SignedSupportingTokens> <ramp:RampartConfig xmlns:ramp="http://ws.apache.org/rampart/policy"> <ramp:user>MyUsername</ramp:user> <ramp:passwordCallbackClass>s.e.r.PasswordCallbackHandler</ramp:passwordCallbackClass> </ramp:RampartConfig> </wsp:All> </wsp:ExactlyOne> </wsp:Policy>
Основной частью в этой политике является указание имени пользователя целевого сервиса и класса, который в нашем случае просто подставит необходимый пароль, класс s.e.r.PasswordCallBackHandler:
package s.e.r; import org.apache.ws.security.WSPasswordCallback; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.UnsupportedCallbackException; import java.io.IOException; public class PasswordCallBackHandler implements CallbackHandler { private static final String PASSWORD = "MyPassword"; @Override public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { for (Callback callback : callbacks) { if (callback instanceof WSPasswordCallback) { WSPasswordCallback pc = (WSPasswordCallback) callback; if (pc.getUsage() == WSPasswordCallback.USERNAME_TOKEN_UNKNOWN) { if (pc.getPassword().equals(PASSWORD)) return; throw new UnsupportedCallbackException(callback, "Check failed"); } pc.setPassword(PASSWORD); } else { throw new UnsupportedCallbackException(callback, "Unrecognized Callback"); } } } }
Пакуем в jar и кладём в WSO2_HOME/repository/components/lib/.
Рестартуем WSO2.
Политика для входящих сообщений
Указываем имя: empty-policy.
Указываем политику:
<wsp:Policy xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="UTOverTransport"> <wsp:ExactlyOne> <wsp:All> <sp:TransportBinding xmlns:sp="http://schemas.xmlsoap.org/ws/2005/07/securitypolicy"> <wsp:Policy/> </sp:TransportBinding> <sp:SignedSupportingTokens xmlns:sp="http://schemas.xmlsoap.org/ws/2005/07/securitypolicy"> <wsp:Policy/> </sp:SignedSupportingTokens> </wsp:All> </wsp:ExactlyOne> </wsp:Policy>
Создаём Address Endpoint
Указываем имя: my-endpoint.
Указываем адрес сервиса.
В дополнительный опциях отмечаем использование WS-Security и указываем политики для исходящих и входящих сообщений созданные ранее.
Исходник Endpoint’а:
<endpoint name="my-endpoint"> <address uri="http://service/soap/"> <enableSec inboundPolicy="empty-policy" outboundPolicy="service-policy"/> </address> </endpoint>
Создаём XSLT-преобразование сообщений
XSLT-преобразования создаются там же где и политики: Manage — Service Bus — Local Entries — Add Local Entries — Add In-lined XML Entry.
Преобразование входящего сообщения
Нам необходимо привести запрос нашего приложения к виду, который понимает сервис:
Указываем имя: in-xslt
Указываем преобразование:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:template match="/"> <!--тут само преобразование--> </xsl:template> </xsl:stylesheet>
Преобразование в исходящее сообщение
Нам нужно ответ сервиса привести к виду, понятному для приложения. Шаг идентичен шагу для входящего сообщения, только имя: out-xslt.
Создаём Proxy Service
В UI: Manage — Service — Add — Proxy Service — Custom Proxy
Указываем имя: external-service. По этому имени будет доступен ваш сервис на шине, например: localhost:8280/services/external-service.
В нашем случае снимаем отметку с протокола https.
Дальше переключаемся в режим исходно кода (switch to source view) и приводим содержимое приблизительно к следующему:
<?xml version="1.0" encoding="UTF-8"?> <proxy xmlns="http://ws.apache.org/ns/synapse" name="external-service" transports="http" statistics="disable" trace="disable" startOnLoad="true"> <target faultSequence="fault" endpoint="my-endpoint"> <inSequence> <xslt key="in-xslt"/> <send> <endpoint key="my-endpoint"/> </send> </inSequence> <outSequence> <xslt key="out-xslt"/> <send/> </outSequence> </target> <description/> </proxy>
Тут указаны две последовательности для входящего и исходящего сообщения, обе начинаются с соответствующего преобразования и затем посылают сообщение в нужном направлении.
Готово!
Дополнительно
Чтобы следить за тем, во что превращает наша прокси сообщения, использовал fiddler
Потому что не нашёл как настроить и где смотреть конечные сообщения.
Подробнее про fiddler:
Прокси пробрасывает заголовок Action пришедшие из приложения, а сервис ругается?
Добавляем медиаторы в последовательность прокси сервиса, удаляем заголовок Action:
<inSequence> <xslt key="in-xslt"/> <header name="Action" value=""/> <property name="SOAPAction" value="" scope="transport"/> <send> <endpoint key="my-endpoint"/> </send> </inSequence>
Прокси возвращает данные в некорректной кодировке?
Указываем кодировку, в которой возвращаем сообщения:
<outSequence> <xslt key="out-xslt"/> <property name="messageType" value="text/xml;charset=windows-1251" scope="axis2" type="STRING"/> <send/> </outSequence>
Ответ сервиса состоит из нескольких частей?
Например:
<soapenv:Envelope> <soapenv:Body> <part1/> <part2/> </soapenv:Body> </soapenv:Envelope>
WSO2 по умолчанию считает что ответ должен состоять из одной части, поэтому передают на преобразование то что находится по пути: s11:Body/child::*[position()=1] | s12:Body/child::*[position()=1], как результат мы можем трансформировать только первую часть, чтобы это исправить меняем вызов преобразователя в сервисе:
<outSequence> <xslt xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" key="stav-out-xslt" source="SOAP-ENV:Body"/> <send/> </outSequence>
… и учитываем это в преобразователе:
<xsl:stylesheet> <xsl:template match="."> <xsl:apply-templates select="/part1"/> <xsl:apply-templates select="/part2"/> </xsl:template> <!--в нашем случае part1 нужно было проигнорировать--> <xsl:template match="part1"/> <xsl:template match="part2"> <forbody> <!--тут трансформации обёрнутые в произвольный тег, он необходим, чтоб в конечном результате не пропал корневой элемент ответа--> </forbody> </xsl:template> </xsl:stylesheet>
Нерешённые вопросы
Пришлось настраивать две политики безопасности
Есть ощущение что можно обойтись одной, но с единой политикой выходила ошибка, о том, что ответ сервиса не содержит заголовков безопасности. А он действительно их не содержит. Чтоб это обойти пришлось писать пустую политику для ответа.
Не найдено встроенного решения для хранения учётных данных стороннего сервиса
Неуклюжее решение с классом для хранения пароля очевидный костыль, который нужно заменить подходящим решением, либо встроенным, либо велосипедом.
Итог
- Часть с преобразованием не вызвала проблем, кроме того, что настройка очень сильно размазана по UI. Конечно можно настраивать через единый xml в том же UI, но в этом случае настройка будет размазана по xml.
- Довольно высокий порог вхождения для решения простейшей задачи.
- Задача решена!
ссылка на оригинал статьи http://habrahabr.ru/post/212267/
Добавить комментарий