Порядок действия будет следующим:
- сгенерировать ключ безопасности
- добавить поддержку 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/
Добавить комментарий