Pull to refresh

ContactManager, часть 5. Добавляем работу через HTTPS

Reading time 4 min
Views 11K
Перед тем как отправить наш REST-сервис в свободное плавание и сделать его общедоступным, нужно позаботиться об усилении безопасности и обеспечить работу через HTTPS. В качестве контейнера сервлетов мы используем Tomcat 7.

Порядок действия будет следующим:
  • сгенерировать ключ безопасности
  • добавить поддержку HTTS в Tomcat
  • добавить поддержку HTTS в SpringSecurity
  • протестировать (а как же без этого)


Генерируем ключ безопасности

Сгенерировать ключ нам поможет утилита keytool из стандартной поставки JRE. Если JAVA_HOME добавлена в path, то просто запускаем keytool из командной строки, если нет — то переходим в каталог %JAVA_HOME%/bin и запускаем keytool оттуда. Для MS Windows команда будет выглядеть примерно так:
keytool -genkey -alias ContactManager -keyalg RSA -keystore c:/contactmanager.keystore

alias — уникальный идентификатор ключа
keyalg — алгоритм генерации. Возможные значения RSA, DSA, DES
keystore — путь к файлу

После запуска программа попросит ввести пароль и несколько параметров, пароль желательно запомнить, он нам ещё пригодится, остальные значения могут быть произвольными: кто, что, откуда, страна и проч. В итоге мы получим файл на диске в указанной директории. Ключ готов.

Изменяем настройки Томката

Открываем файл %CATALINA_HOME%/conf/server.xml и находим закомментированный кусок
    <!-- Define a SSL HTTP/1.1 Connector on port 8443
         This connector uses the JSSE configuration, when using APR, the
         connector should be using the OpenSSL style configuration
         described in the APR documentation -->
    <!--
    <Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
               maxThreads="150" scheme="https" secure="true"
               clientAuth="false" sslProtocol="TLS" />
    -->

Убираем комментарии с элемента Connector и добавляем пару атрибутов для нашего ключа:
    <Connector port="8443" 
		SSLEnabled="true"
		protocol="HTTP/1.1"
		maxThreads="150" scheme="https" secure="true"
		keystoreFile="c:\contactmanager.keystore"
		keystorePass="password"
		sslProtocol="TLS" />

keystorePass — пароль, который мы ввели при генерации ключа. Да, он хранится в открытом виде. Есть способы решения этой проблемы, но пока оставим так. Собственно все, можно запускать. Упс…
INFO: Initializing ProtocolHandler ["http-apr-8080"]
мар 28, 2013 11:43:04 AM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-apr-8443"]
мар 28, 2013 11:43:04 AM org.apache.coyote.AbstractProtocol init
SEVERE: Failed to initialize end point associated with ProtocolHandler ["http-apr-8443"]
java.lang.Exception: Connector attribute SSLCertificateFile must be defined when using SSL with APR
        at org.apache.tomcat.util.net.AprEndpoint.bind(AprEndpoint.java:507)
        ...

Не получилось. Гугление дает ответ, что protocol="HTTP/1.1" нужно заменить на protocol="org.apache.coyote.http11.Http11Protocol". Запускаемся, теперь все в порядке.
...
мар 28, 2013 11:56:41 AM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-apr-8080"]
мар 28, 2013 11:56:41 AM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-bio-8443"]
мар 28, 2013 11:56:41 AM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["ajp-apr-8009"]
мар 28, 2013 11:56:41 AM org.apache.catalina.startup.Catalina load
INFO: Initialization processed in 1909 ms
...

При переходе по адресу https://localhost:8443/ браузер предупреждает о сомнительности нашего сертификата, но мы его предупреждения игнорируем, жмем «продолжить на свой страх и риск» и видим корневую страницу Томката.

Настраиваем Spring Security

Здесь тоже все довольно просто. В файле security.xml в каждый из критичных урлов веб-сервиса нужно добавить атрибут requires-channel="https". Выглядеть это будет так:
<intercept-url pattern="/ws/index*" access="hasAnyRole('ROLE_USER','ROLE_ANONYMOUS')" requires-channel="https"/>
<intercept-url pattern="/ws/add*" access="hasRole('ROLE_USER')" requires-channel="https"/>
<intercept-url pattern="/ws/delete/*" access="hasRole('ROLE_ADMIN') " requires-channel="https"/>

Тестируем

Ресурс /ws/index мы тоже спрятали за HTTPS, поэтому попробуем выполнить тест index_user1(). Ошибка, что, впрочем, ожидаемо. Вопрос, что за ошибка и как её исправить. JUnit ругается на кривой ответ
com.fasterxml.jackson.databind.JsonMappingException: No content to map due to end-of-input
 at [Source: java.io.StringReader@1841d1d3; line: 1, column: 1]

но понятно, что дело не в этом. Смотрим лог в консоли, там уже более интересно, есть статус ошибки, 302:
...
MockHttpServletResponse:
              Status = 302
       Error message = null
             Headers = {Location=[https://localhost/ws/index]}
        Content type = null
                Body = 
       Forwarded URL = null
      Redirected URL = https://localhost/ws/index
             Cookies = []

Видимо, мы как-то не так формируем запрос в тесте. Отправляемся в билдер MockHttpServletRequestBuilder и изучаем список его методов, ищем что-то связанное с безопасностью. Ага, вот оно.
	/**
	 * Set the secure property of the {@link ServletRequest} indicating use of a
	 * secure channel, such as HTTPS.
	 *
	 * @param secure whether the request is using a secure channel
	 */
	public MockHttpServletRequestBuilder secure(boolean secure){
		this.secure = secure;
		return this;
	}

Похоже, то, что нужно. Добавляем этот метод в цепочку вызовов в билдере
		def result = mockMvc.perform(MockMvcRequestBuilders.get("/ws/index")
				.secure(true) // <--------- добавляем работу через HTTPS
				.with(SecurityRequestPostProcessors.userDetailsService(USER1)))
				.andDo(MockMvcResultHandlers.print())
				.andReturn()

Ура, работает! Отлично. Изменяем остальные WS-тесты аналогичным образом. Теперь мы передаем авторизационные данные по защищенному соединению и можем смело выкладывать наш REST-сервис вовне. Но это касается только REST-запросов, старая Form-based аутентификация у нас никак не защищена и остается уязвимым местом. Решить эту задачу предлагаю самостоятельно.

Что можно сделать ещё? Сейчас мы вынуждены указывать логин и пароль при каждом запросе к защищенному ресурсу. Плюс пользователи жестко прописаны в файле seciruty.xml, а вдруг (хотя почему вдруг?) наш сервис станет популярным? Поэтому в следующей итерации мы сделаем следующее: перенесем данные о пользователе в БД и изменим схему аутентификации на работу с Auth Token, в котором будем хранить данные о сессии пользователя.

Продолжение следует.
Tags:
Hubs:
+5
Comments 4
Comments Comments 4

Articles