В этой статье я собираюсь показать, как выполнить модульное тестирование конечной точки HTTPS с помощью Apache HttpClient . Кроме того, я покажу, как настроить конечную точку HTTPS, используя Mule ESB в качестве фиктивного HTTPS-сервера.
Пример в этой статье ограничен показом того, как клиент будет утверждать идентичность сервера с помощью склада доверенных сертификатов. Сервер не будет утверждать личность клиента (взаимная аутентификация).
Создать пример проекта
Я собираюсь разработать пример проекта в Spring Tool Suite с установленным плагином Anypoint Studio . Основная причина этого заключается в том, чтобы иметь возможность удобно и быстро создать проект с соответствующими зависимостями Mule. Если вы можете собрать необходимые зависимости каким-либо другим способом, не стесняйтесь использовать IDE по вашему выбору.
Если вы используете плагин Anypoint Studio, вам нужно установить хотя бы одну среду исполнения Mule. Я использовал среду выполнения 3.5.0. Версия 3.5.0 Mule CE содержит версию 4.2 библиотеки Apache HttpClient, это версия HttpClient, которую я выбрал.
Давайте начнем с создания примера проекта:
- В меню File выберите New и затем Other.
В появившемся диалоговом окне выберите мастер проекта Mule.
- В диалоговом окне «Параметры проекта» введите имя проекта и выберите среду выполнения Mule, которую будет использовать проект. Я назову свой проект «HTTPSUnitTestWithHTTPClient» и, как и раньше, использую среду выполнения Mule 3.5.0 CE.
Теперь мы создали пример проекта. Прежде чем мы реализуем что-либо в этом проекте, мы собираемся создать хранилище ключей сервера и хранилище доверенных сертификатов клиента.
Создайте хранилище ключей сервера
Хранилище ключей сервера — это файл, который содержит закрытый ключ сервера. Этот ключ используется, когда клиенты договариваются о соединении с сервером, чтобы клиенты могли проверить, что сервер, к которому он подключается, действительно является сервером, к которому клиент хочет подключиться, а не обманщиком. Кроме того, согласование HTTPS приводит к зашифрованному соединению, обеспечивающему обмен информацией между клиентом и сервером, который защищен от перехвата.
Чтобы создать хранилище ключей сервера, я использовал следующие команды из терминала или командного окна:
keytool-keypass secret-storepass secret-genkey-aliashttpskey-keyalg RSA-keystore server_keystore.jks
После ввода команды вам будет задан ряд вопросов. Ответьте на них, как вы хотите. Ниже я ответил на вопросы при создании хранилища ключей на моем сервере.
What is your first and last name? [Unknown]: Ivan Krizsan What is the name of your organizational unit? [Unknown]: Home Unit What is the name of your organization? [Unknown]: Home Organization What is the name of your City or Locality? [Unknown]: Home City What is the name of your State or Province? [Unknown]: Home State What is the two-letter country code for this unit? [Unknown]: SE Is CN=Ivan Krizsan, OU=Home Unit, O=Home Organization, L=Home City, ST=Home State, C=SE correct? [no]: yes
Результатом должен быть файл с именем «server_keystore.jks».
Создайте клиентское хранилище доверенных сертификатов
Склад доверенных сертификатов клиента содержит одну или несколько записей для сертификатов, которым клиент должен доверять. Когда клиент пытается подключиться к серверу, удостоверение сервера проверяется по записям в хранилище доверенных сертификатов.
Чтобы создать клиентское хранилище доверенных сертификатов, я использовал следующие две команды:
keytool -export -alias httpskey -keystore server_keystore.jks -storepass secret -file server.cert keytool -import -v -trustcacerts -alias httpskey -keystore client_truststore.jks -storepass secret -file server.cert
После ввода второй команды вас спросят, доверять ли сертификату в файле server.cert. Ответьте да на этот вопрос.
Теперь у вас должно быть всего три файла: «server_keystore.jks», «client_truststore.jks» и «server.cert». Скопируйте первые два файла в «src / main / resources» примера проекта, который мы создали ранее.
Создать макет конфигурации сервера
В примере проекта должен быть файл с именем «httpsunittestwithhttpclient.xml» в каталоге «src / main / app». Вставьте следующую конфигурацию Mule в этот файл:
<?xml version="1.0" encoding="UTF-8"?> <mule xmlns:https="http://www.mulesoft.org/schema/mule/https" xmlns="http://www.mulesoft.org/schema/mule/core" xmlns:doc="http://www.mulesoft.org/schema/mule/documentation" xmlns:spring="http://www.springframework.org/schema/beans" xmlns:test="http://www.mulesoft.org/schema/mule/test" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-current.xsd http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd http://www.mulesoft.org/schema/mule/https http://www.mulesoft.org/schema/mule/https/current/mule-https.xsd http://www.mulesoft.org/schema/mule/test http://www.mulesoft.org/schema/mule/test/current/mule-test.xsd"> <https:connector name="HTTPSNoClientCert" clientSoTimeout="10000" serverSoTimeout="10000" socketSoLinger="0"> <!-- Removes the Mule session information from messages sent over this connector. --> <service-overrides sessionHandler="org.mule.session.NullSessionHandler"/> <!-- Sets the keystore and passwords used by this connector. --> <https:tls-key-store path="server_keystore.jks" keyPassword="secret" storePassword="secret" /> </https:connector> <flow name="https-noclientcert-flow"> <https:inbound-endpoint exchange-pattern="request-response" host="localhost" port="8082" connector-ref="HTTPSNoClientCert"/>
Если вы знакомы с файлами конфигурации Mule, не стесняйтесь пропустить следующее объяснение.
Приведенный выше файл конфигурации Mule содержит:
- Разъем HTTPS с именем «HTTPSNoClientCert».
Разъем Mule содержит конфигурацию для средства связи. В этом случае для связи используется базовый протокол HTTPS с некоторыми параметрами времени ожидания и хранилище ключей.
Кроме того, на соединителе настроен нулевой обработчик сеанса, который предотвратит отправку Mule информации сеанса Mule клиентам. - Поток с именем «https-noclientcert-flow».
Поток Мула описывает потенциальный путь, по которому может пройти сообщение. - Поток в свою очередь содержит входящую конечную точку.
Входящая конечная точка является получателем сообщений, отправляемых потоку.
Как можно видеть, поток ссылается на упомянутый ранее HTTPS-коннектор с использованием атрибута connector-ref. Атрибуты хоста и порта указывают адрес и порт, на котором входящая конечная точка будет прослушивать входящие сообщения. - После входящей конечной точки поток содержит тестовый компонент.
Его цель — записывать полученные сообщения на консоль. - Наконец, поток содержит процессор, который устанавливает полезную нагрузку (исходящего) сообщения.
Мы можем протестировать фиктивный HTTPS-сервер, щелкнув правой кнопкой мыши на файле конфигурации Mule, который мы только что создали, и выбрав Run As → Mule Application. Затем откройте браузер, например Firefox, и введите URL-адрес «https: // localhost: 8082 /».
Может быть предупреждение о ненадежном сертификате, но просто подтвердите, что вы доверяете сертификату. Сделав это, вы должны увидеть такую строку в окне браузера, если все работает как положено:
Hello, the time is now 2014-10-05T21:28:19.948+02:00
Добавьте файл конфигурации Log4J
Чтобы увидеть, что регистрирует Mule, добавьте следующий файл с именем «log4j.xml» в каталог «src / test / resources» в примере проекта:
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration> <appender name="stdout" class="org.apache.log4j.ConsoleAppender"> <param name="Target" value="System.out" /> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d %-5p %-30C - %m%n" /> </layout> </appender> <logger name="org.mule"> <level value="INFO"/> </logger> <logger name="org.mule.transport"> <level value="WARN"/> </logger> <logger name="org.mule.tck.functional"> <level value="DEBUG"/> </logger> <root> <priority value="WARN" /> <appender-ref ref="stdout" /> </root> </log4j:configuration>
Выполнить тест
Пока что мы только готовились к реальной цели этой статьи, а именно к тесту, показывающему, как подключиться к серверу по HTTPS. Причина, по которой я предпочитаю подключение Apache HttpClient к серверу, например, через клиентский API-интерфейс Mule, заключается в том, что он дает мне больший контроль над запросом, отправляемым на сервер, и я также знаю, что ответ, полученный в моем тестовом коде, имеет не были изменены в любом случае.
Тест, который подключается к серверу-макету, выглядит следующим образом:
package se.ivankrizsan.mule.https; import java.security.KeyStore; import java.util.HashMap; import java.util.Map; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; import org.apache.commons.io.IOUtils; import org.apache.http.Header; import org.apache.http.HeaderIterator; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.impl.client.DefaultHttpClient; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mule.tck.junit4.FunctionalTestCase; /** * Tests connecting to a HTTPS server that does not require a client certificate. * * @author Ivan Krizsan */ public class HttpsNoClientCertificateTest extends FunctionalTestCase { /* Constant(s): */ /** Mule configuration file(s) loaded by the test-case. */ private final static String[] TEST_CONFIGURATION_FILES = { "httpsunittestwithhttpclient.xml" }; /** URL which to send test-request to. */ private final static String TEST_ENDPOINT_URL = "https://localhost:8082/"; /** Client truststore. */ private static final String CLIENT_TRUSTSTORE = "client_truststore.jks"; /** Client truststore password. */ private static final String CLIENT_TRUSTSTORE_PASSWORD = "secret"; /* Instance variable(s): */ private DefaultHttpClient mHttpClient; @Override protected String[] getConfigFiles() { return TEST_CONFIGURATION_FILES; } /** * Prepares for tests by performing creating and initializing the HTTP * client used to send test-request. * * @throws Exception If error occurred. */ @Before public void setUp() throws Exception { mHttpClient = new DefaultHttpClient(); /* Load client truststore. */ final KeyStore theClientTruststore = KeyStore.getInstance("JKS"); theClientTruststore.load(loadResource(CLIENT_TRUSTSTORE), CLIENT_TRUSTSTORE_PASSWORD.toCharArray()); /* Create a trust manager factory using the client truststore. */ final TrustManagerFactory theTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory .getDefaultAlgorithm()); theTrustManagerFactory.init(theClientTruststore); /* * Create a SSL context with a trust manager that uses the * client truststore. */ final SSLContext theSslContext = SSLContext.getInstance("TLS"); theSslContext.init(null, theTrustManagerFactory.getTrustManagers(), null); /* * Create a SSL socket factory that uses the client truststore SSL * context and that does not perform any kind of hostname verification. * IMPORTANT: Hostname verification should be performed in a * production environment! * To turn on hostname verification, change the * ALLOW_ALL_HOSTNAME_VERIFIER below to STRICT_HOSTNAME_VERIFIER. */ final SSLSocketFactory theSslSocketFactory = new SSLSocketFactory(theSslContext, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); /* * Register the SSL socket factory to be used with HTTPS connections * with the HTTP client. * A {@code Scheme} object is used to associate the protocol scheme, * such as HTTPS in this case, and a socket factory. */ final Scheme theHttpsScheme = new Scheme("https", 443, theSslSocketFactory); mHttpClient.getConnectionManager().getSchemeRegistry().register( theHttpsScheme); } /** * Tests a successful HTTPS connection. * * @throws Exception If error occurs. Indicates test failure. */ @Test public void testSuccessfulConnection() throws Exception { /* * Create a HTTP GET to the HTTPS endpoint and set a HTTP header * on the request. */ final HttpGet theHttpGet = new HttpGet(TEST_ENDPOINT_URL); theHttpGet.addHeader("test-header-name", "test-header-value"); /* Send the HTTPS request. */ final HttpResponse theHttpResponse = mHttpClient.execute(theHttpGet); Assert.assertNotNull("A response message should have been received", theHttpResponse); /* Insert headers from the response in a map. */ final Map<String, String> theResponseHeadersMap = new HashMap<String, String>(); final HeaderIterator theHeaderIterator =
Код довольно хорошо прокомментирован, но я, несмотря на это, приведу краткий анализ:
- Тест-класс наследуется от класса FunctionalTestCase .
FunctionalTestCase — это базовый класс для тестов, который хочет запустить экземпляр Mule как часть тестов. - Метод getConfigFiles возвращает файлы конфигурации Mule, которые должны быть загружены в экземпляр Mule, запущенный как часть тестов.
В данном случае это файл конфигурации Mule, содержащий фиктивный сервер. - Метод setUp создает и настраивает экземпляр Apache HttpClient, который использует определенное хранилище доверенных сертификатов для проверки подлинности серверов, с которыми он связывается.
- Тест-метод testSuccessfulConnection проверяет, что ожидается успешной попыткой подключения к серверу.
Выполнение теста
Если мы теперь щелкнем правой кнопкой мыши по классу test в IDE и выберем Run As → JUnit Test, тест должен пройти, и вы увидите, среди прочего, вывод, похожий на этот в консоли:
******************************************************************************** * Message Received in service: https-noclientcert-flow. Content is: / * ******************************************************************************** 2014-10-05 22:00:11,845 INFO org.mule.module.logging.DispatchingLogger - Full Message payload: / Message properties: INVOCATION scoped properties: INBOUND scoped properties: Connection=true Host=localhost:8082 Keep-Alive=true MULE_ORIGINATING_ENDPOINT=endpoint.https.localhost.8082 MULE_REMOTE_CLIENT_ADDRESS=/127.0.0.1:50474 User-Agent=Apache-HttpClient/4.2 (java 1.5) http.context.path=/ http.context.uri=https://localhost:8082 http.headers={Host=localhost:8082, User-Agent=Apache-HttpClient/4.2 (java 1.5), Keep-Alive=true, test-header-name=test-header-value, Connection=true} http.method=GET http.query.params={} http.query.string= http.relative.path= http.request=/ http.request.path=/ http.version=HTTP/1.1 test-header-name=test-header-value OUTBOUND scoped properties: LOCAL_CERTIFICATES=[Ljava.security.cert.X509Certificate;@76c5ccf MULE_ENCODING=UTF-8 SESSION scoped properties: Response headers: {LOCAL_CERTIFICATES=[Ljava.security.cert.X509Certificate;@76c5ccf, X-MULE_ENCODING=UTF-8, Date=Sun, 05 Oct 2014 22:00:11 +0200, Content-Length=52, Connection=close, Content-Type=tex t/plain, Server=Mule EE Core Extensions/3.5.0} Response body: Hello, the time is now 2014-10-05T22:00:11.859+02:00
Из вышесказанного видно, что запрос с HTTP-заголовком «test-header-name» со значением «test-header-value» был получен фиктивным сервером. Мы также можем видеть заголовки ответа и полезную нагрузку ответа, которая содержит строку приветствия того же типа, которую мы видели в браузере при тестировании фиктивного сервера.
Обратите внимание, что мы также видим, что библиотека Apache HttpClient использовалась для отправки запроса на сервер путем поиска HTTP-заголовка «User-Agent» со значением «Apache-HttpClient / 4.2 (java 1.5)».