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

от автора

Перед тем как отправить наш 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, в котором будем хранить данные о сессии пользователя.

Продолжение следует.

ссылка на оригинал статьи http://habrahabr.ru/post/174513/