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