На прошлой неделе я написал учебник по реализации безопасности в Java EE 6 . На этой неделе я хотел бы показать вам, как реализовать те же функции с помощью Spring Security . Прежде чем начать, я хотел бы объяснить причину написания этой статьи.
В прошлом месяце я выступил с докладом о безопасности веб-приложений на Java в Utah JUG (UJUG). В рамках этой презентации я сделал несколько демонстраций о том, как реализовать безопасность с помощью Java EE 6, Spring Security и Apache Shiro. Я сказал аудитории, что опубликую презентацию, и планировал записывать скриншоты различных демонстраций, чтобы онлайн-версия презентации имела больше смысла.
Сегодня я закончил второй скринкаст, показывающий, как реализовать безопасность с помощью Spring Security. Ниже представлена презентация (с включенной на слайде 16 скринкастом), а также пошаговое руководство.
Учебник по входу в систему Spring Security
- Загрузите и запустите приложение
- Реализовать базовую аутентификацию
- Принудительно SSL
- Реализовать аутентификацию на основе форм
- Добавить Запомнить меня
- Хранить пользователей в базе данных
- Резюме
Загрузите и запустите приложение.
Для начала загрузите приложение, в котором вы будете внедрять систему безопасности. Это приложение является урезанной версией приложения Ajax Login, которое я написал для своей статьи о реализации Ajax-аутентификации с использованием jQuery, Spring Security и HTTPS . Для запуска приложения вам потребуется Java 6 и Maven. Запустите его, используя mvn jetty: run и откройте http: // localhost: 8080 в вашем браузере. Вы увидите, что это простое приложение CRUD для пользователей, и для добавления или удаления пользователей не требуется вход в систему.
Реализация базовой аутентификации
Первым шагом является защита экрана списка, чтобы людям приходилось входить в систему для просмотра пользователей. Для этого вам нужно создать контекстный файл Spring, содержащий конфигурацию Spring Security. Создайте src / main / webapp / WEB-INF / security.xml и заполните его следующим содержанием:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd">
<!-- New in Spring Security 3.1 -->
<!-- <http pattern="/css/**" security="none"/> -->
<http auto-config="true">
<intercept-url pattern="/app/users" access="ROLE_USER,ROLE_ADMIN"/>
<http-basic/>
</http>
<authentication-manager alias="authenticationManager">
<authentication-provider>
<password-encoder hash="sha"/>
<user-service>
<user name="user" password="12dea96fec20593566ab75692c9949596833adc9" authorities="ROLE_USER"/>
<user name="admin" password="d033e22ae348aeb5660fc2140aec35850c4da997" authorities="ROLE_ADMIN"/>
</user-service>
</authentication-provider>
</authentication-manager>
<!-- Override userSecurityAdvice bean in appfuse-service to allow any role to update a user. -->
<beans:bean id="userSecurityAdvice" class="org.appfuse.examples.webapp.security.UserSecurityAdvice"/>
</beans:beans>
Последний компонент, userSecurityAdvice, является аспектом, который необходим для переопределения некоторого поведения в AppFuse . Это обычно не требуется при реализации Spring Security.
Затем откройте src / main / webapp / WEB-INF / web.xml и добавьте DelegatingFilterProxy в Spring:
<filter> <filter-name>securityFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetBeanName</param-name> <param-value>springSecurityFilterChain</param-value> </init-param> </filter>
И добавьте его сопоставление фильтра сразу после rewriteFilter в разделе сопоставления фильтра (порядок важен!):
<filter-mapping> <filter-name>rewriteFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>securityFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> <dispatcher>INCLUDE</dispatcher> </filter-mapping>
Вам не нужно добавлять какие-либо зависимости в ваш pom.xml, потому что этот проект зависит от AppFuse, который уже содержит эти зависимости.
На этом этапе, если вы перезапустите Jetty (Ctrl + C и Jetty: запустите снова), вам будет предложено войти в систему, когда вы нажмете на вкладку «Пользователи». Введите admin / admin для входа. Spring Security немного проще в настройке, чем Java EE 6, в основном из-за того, что он не требует настройки вашего контейнера.
После входа в систему вы можете попытаться выйти, нажав ссылку «Выйти» в правом верхнем углу. Это вызывает LogoutController со следующим кодом, который выводит пользователя из системы.
public void logout(HttpServletResponse response) throws ServletException, IOException {
request.getSession().invalidate();
response.sendRedirect(request.getContextPath());
}
ПРИМЕЧАНИЕ. В Spring Security есть способ настроить выход из системы в соответствии с URL-адресом и избавиться от такого класса, как LogoutController. Поскольку это уже было в проекте, я не расскажу об этом в этом уроке.
Вы заметите, что нажатие на эту ссылку не приведет к выходу из системы, даже если сеанс признан недействительным. Единственный способ выйти из системы с базовой аутентификацией — закрыть браузер. Чтобы получить возможность выхода из системы, а также получить больший контроль над внешним видом входа в систему, вы можете реализовать аутентификацию на основе форм. Перед тем, как вы внедрите аутентификацию на основе форм, я хотел бы показать вам, как легко использовать SSL с помощью Spring Security.
Force SSL
Spring Security позволяет переключаться между безопасными (https) и незащищенными (http) протоколами, используя простой атрибут require-channel в элементе <intercept-url>. Возможные значения: «http», «https» и «any». Добавьте require-channel = «https» в свой файл security.xml:
<intercept-url pattern="/app/users" access="ROLE_USER,ROLE_ADMIN" requires-channel="https"/>
Чтобы это работало, вы должны настроить Jetty на прослушивание порта SSL. Добавьте следующее сразу после элемента </ webAppConfig> для jetty-maven-plugin в вашем файле pom.xml:
<connectors>
<connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
<forwarded>true</forwarded>
<port>8080</port>
</connector>
<connector implementation="org.eclipse.jetty.server.ssl.SslSelectChannelConnector">
<forwarded>true</forwarded>
<port>8443</port>
<maxIdleTime>60000</maxIdleTime>
<keystore>${project.build.directory}/ssl.keystore</keystore>
<password>appfuse</password>
<keyPassword>appfuse</keyPassword>
</connector>
</connectors>
Хранилище ключей должно быть сгенерировано для успешного запуска Jetty, поэтому добавьте keytool-maven-plugin чуть выше jetty-maven-plugin в pom.xml.
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>keytool-maven-plugin</artifactId>
<version>1.0</version>
<executions>
<execution>
<phase>generate-resources</phase>
<id>clean</id>
<goals>
<goal>clean</goal>
</goals>
</execution>
<execution>
<phase>generate-resources</phase>
<id>genkey</id>
<goals>
<goal>genkey</goal>
</goals>
</execution>
</executions>
<configuration>
<keystore>${project.build.directory}/ssl.keystore</keystore>
<dname>cn=localhost</dname>
<keypass>appfuse</keypass>
<storepass>appfuse</storepass>
<alias>appfuse</alias>
<keyalg>RSA</keyalg>
</configuration>
</plugin>
Теперь, если вы перезапустите Jetty, перейдите по адресу http: // localhost: 8080 и перейдите на вкладку «Пользователи», вам будет предложено принять ненадежный сертификат, а затем после входа будет перенаправлен на https: // localhost: 8443 / users Это улучшение ограничения пользовательских данных Java EE по двум причинам:
- Вы можете переключаться между протоколами http и https. С Java EE, вы можете только заставить https. Вы должны написать собственный фильтр, чтобы переключиться обратно на http.
- Перенаправление на https действительно работает. В Java EE (по крайней мере, на Jetty) вместо перенаправления запроса возвращается 403.
Теперь давайте рассмотрим, как лучше контролировать внешний вид экрана входа в систему, а также как заставить выход из системы работать с аутентификацией на основе форм.
Реализация проверки подлинности на основе форм
Чтобы перейти от базовой проверки подлинности на основе форм, просто добавьте элемент <form-login> в элемент <http> security.xml:
<http auto-config="true">
<intercept-url pattern="/app/users" access="ROLE_USER,ROLE_ADMIN" requires-channel="https"/>
<form-login login-page="/login" authentication-failure-url="/login?error=true"
login-processing-url="/j_security_check"/>
<http-basic/>
</http>
Вы можете оставить элемент <http-basic>, поскольку Spring Security достаточно умен, чтобы обслуживать форму для браузеров и использовать базовую аутентификацию для клиентов, таких как веб-сервисы. Страница login.jsp (которую перенаправляет / login) уже существует в проекте, в каталоге src / main / webapp . Пересылка выполняется UrlRewriteFilter со следующей конфигурацией в src / main / webapp / WEB-INF / urlrewrite.xml .
<rule>
<from>/login</from>
<to>/login.jsp</to>
</rule>
Этот JSP имеет 3 важных элемента: 1) форму, которая представляет «/ j_security_check», 2) элемент ввода с именем «j_username» и 3) элемент ввода с именем «j_password». Если вы перезапустите Jetty, вам будет предложено войти в систему с помощью этого JSP вместо диалогового окна базовой аутентификации.
Добавить Запомнить меня
Запомнить меня — это функция, которую вы видите во многих веб-приложениях сегодня. Обычно это флажок в форме входа, который позволяет автоматически входить при следующем посещении сайта. Эта функция не существует в безопасности Java EE, но она существует в Spring Security. Чтобы включить его, добавьте следующее чуть ниже <form-login> в security.xml:
<remember-me user-service-ref="userDao" key="e37f4b31-0c45-11dd-bd0b-0800200c9a66"/>
Затем откройте src / main / webapp / login.jsp и измените имя флажка «запомнить меня» на _spring_security_remember_me :
<input type="checkbox" name="_spring_security_remember_me" id="rememberMe"/>
После внесения этих изменений вы сможете перезагрузить Jetty, перейти по адресу http: // localhost: 8080 / users , ввести admin / adminjdbc, установить флажок «Запомнить меня» и войти в систему. Затем закройте браузер и повторите процесс. На этот раз вам не будет предложено войти в систему. Для получения дополнительной информации об этой функции см. Документацию Spring Security «Помни меня» .
Хотя хранение имен пользователей и паролей в файле удобно для демонстраций, оно не очень реалистично. В следующем разделе показано, как настроить Spring Security для использования базы данных в качестве хранилища пользователей.
Хранение пользователей в базе данных
Чтобы хранить пользователей в базе данных вместо файла, вам нужно добавить атрибут user-service-ref к элементу <authentication-provider>. Вы также можете удалить элемент <user-service>.
<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="userDao">
<password-encoder hash="sha"/>
</authentication-provider>
</authentication-manager>
Бин «userDao» предоставляется AppFuse и его классом UserDaoHibernate.java . Этот класс реализует интерфейс UserDetailsService Spring Security . В Java EE мне пришлось настроить соединение с базой данных и убедиться, что драйвер JDBC находится в пути к классам моего контейнера. С помощью Spring Security вы можете общаться с базой данных, которую вы уже настроили в своем приложении.
Конечно, вы можете сделать это и с Java EE. В последнем уроке я не учел одну вещь: 1) приложение использует H2 и 2) мне пришлось настроить базу данных Java EE на MySQL. Это произошло потому, что когда я попытался получить доступ к своему экземпляру H2, я получил ошибку о двух потоках, пытающихся получить к нему доступ одновременно.
2011-05-13 08:47:29.081:WARN::UserRealm Java EE Login could not connect to database; will try later
org.h2.jdbc.JdbcSQLException: Database may be already in use: "Locked by another process".
Possible solutions: close all other connection(s); use the server mode [90020-154]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:327)
at org.h2.message.DbException.get(DbException.java:167)
at org.h2.message.DbException.get(DbException.java:144)
at org.h2.store.FileLock.getExceptionAlreadyInUse(FileLock.java:443)
at org.h2.store.FileLock.lockFile(FileLock.java:338)
at org.h2.store.FileLock.lock(FileLock.java:134)
at org.h2.engine.Database.open(Database.java:535)
at org.h2.engine.Database.openDatabase(Database.java:218)
Пароль для пользователя «admin» настраивается в src / test / resources / sample-data.xml и загружается DbUnit перед запуском приложения. Вы можете просмотреть конфигурацию вашего pom.xml и dbunit-maven-plugin, если вам интересно узнать, как это сделать. В настоящее время пароль настроен на «adminjdbc», но вы можете сбросить его, сгенерировав новый пароль и изменив sample-data.xml.
Теперь, если вы перезапустите Jetty, вы сможете войти с помощью admin / adminjdbc и просмотреть список пользователей.
Резюме
В этом руководстве вы узнали, как реализовать аутентификацию с помощью Spring Security 3.0.5. В дополнение к базовой конфигурации XML Spring Security также предоставляет поддержку AOP и аннотации, которые можно использовать для защиты методов. Он также имеет гораздо больше функций, чем стандартная безопасность Java EE. На мой взгляд, это самая зрелая инфраструктура безопасности, которую мы имеем на Java сегодня. В настоящее время, я думаю, что его справочная документация — лучшее место, чтобы узнать больше.
В Spring Security я обнаружил несколько ограничений:
- Механизм аутентификации (файл, база данных, ldap и т. Д.) Содержится в WAR
- Методы защиты работают только на бобах Spring
- «Помни меня» не работает в моей заставке (потому что я забыл переименовать флажок в login.jsp)
Конечно, вы можете настроить Spring для загрузки своей конфигурации вне WAR (например, файла или JNDI), но это не так просто, как включить конфигурацию в ваше приложение.
В ближайшие пару недель я опубликую часть III этой серии, где я покажу вам, как реализовать этот же набор функций с помощью Apache Shiro. А пока, пожалуйста, дайте мне знать, если у вас есть какие-либо вопросы.
Я создал скринкасты с Camtasia . Для небольших экранов и встраивания в презентацию я создал ее на 50% и использовал функцию SmartFocus для увеличения и уменьшения масштаба во время демонстрации. Для больших экранов я опубликовал еще один скринкаст на 100% в HD . Если у вас есть предпочтение, какой скринкаст лучше, я хотел бы услышать об этом.
От http://raibledesigns.com/rd/entry/java_web_application_security_part1