SPRING-SOURCE.RU
Исходный код:
Начальный проект
(Самое первое рабочее приложение)
IP адрес в remember-me
(Делаем приложение более защищенным за счет использования ip адреса пользователя)
Смена пароля
(Добавляем функцию смены пароля с использованием InMemoryDaoImpl)
Последнее обновление 18.08.2010

Повышение пользовательского опыта

  1. Изменим login и logout страницы и свяжем их со стандартным Spring MVC контроллером
  2. Включим функцию "запомни меня" для удобства пользователей и понимания последствий безопасности
  3. Построим управление пользовательским аккаунтом, включая смену пароля и функцию remember me

Смена страницы для входа (login page)

Страница для входа выглядит слишком просто. Давайте возьмемся за ее усовершенствование.

Автоматическая конфигурация не обеспечила нас какими-то либо важными особенностями, включая стилизацию страницы. Поэтому мы решили добавить следующее:

Поток функциональности входа и выхода должен выглядеть как на диаграмме:

Spring Security

Реализация пользовательской страницы для входа

Первым делом мы дожлны заменить страницу для входа Spring Security и интегрировать ее в наш сайт. Поток выглядит следующим образом:

Spring Security

Давайте реализуем login контроллер

Мы должны добавить новый Spring MVC контроллер для обработки, сначала login, а затем logout функциональности. Spring MVC использует механизм аннотаций для настройки контроллеров, настройки путей web сайта и его ресурсов. Давайте создадим контроллер:

                        
// imports omitted
@Controller
public class LoginLogoutController extends BaseController{
    @RequestMapping(method=RequestMethod.GET,value="/login.do")
  public void home() {
  }	
}
		

Вы можете видеть, что мы добавили простой кнтроллер, и связали его с /login.do URL. Суперкласс BaseController (просто пустой класс) позволит нам класть в него методы, которые будут использоваться всеми контроллерами в приложении.

Добавим login JSP страницу

Ссылка /login.do отправит к Spring MVC view resolver (решает какой view использовать), который мы указали в /WEB-INF/dogstore-servlet.xml файле. View resolver используется для поиска JSP страницы под названием login.jsp в /WEB-INF/views. Давайте добавим простой JSP с формой для входа. Во второй части мы изучали два важных элемента формы входа, которые должны быть правильно указаны:

Мы также включим в нашу JSP страницу заголовок и "подвал". Вот, что у нас получилось:

                        
<?xml version="1.0" encoding="ISO-8859-1" ?>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
  pageEncoding="ISO-8859-1"%>
	
<jsp:include page="common/header.jsp">

  <jsp:param name="pageTitle" value="Login" />
</jsp:include>
<h1>Please Log In to Your Account</h1>
<p>Please use the form below to log in to your account.</p>
<form action="j_spring_security_check" method="post"><label
  for="j_username">Login</label>: <input id="j_username"
  name="j_username" size="20" maxlength="50" type="text" /> <br />
<label for="j_password">Password</label>: <input id="j_password"
  name="j_password" size="20" maxlength="50" type="password" /> <br />
<input type="submit" value="Login" /></form>

<jsp:include page="common/footer.jsp" />
		

Помните, что вы должны использовать POST, иначе login запрос будет отклонен UsernamePasswordAuthenticationFilter.

По окончании, мы должны изменить автоматическую настройку Spring Security для пересылки на нашу новую login страницу. Попробуйте набрать в браузере http://localhost:8080/JBCPPets/login.do и посмотрите, что получится.

Что произошло? Вы заметили, что ваш запрос первым был перехвачен Spring Security (перенаправлен на spring_security_login), и и затем вы увидели форму? Это происходит потому, что Spring Security по прежнему относится к странице входа по умолчанию, генерируемой DefaultLoginPageGeneratingFilter классом. Финальный шаг - удалить страницу для входа, которая сделана по умолчанию и использовать нашу персональную страницу.

Настройка Spring Security для использования Spring MVC страницы для входа

На первый взгляд все, что нам нужно - это настроить <form-login> элемент в Spring Security конфигурационном файле и добавить login-page директиву:

                        
<http auto-config="true" use-expressions="true">
  <intercept-url pattern="/*" access="hasRole('ROLE_USER')"/>
  <form-login login-page="/login.do" />
</http>
		

Если сейчас запустить приложение на домешней странице (http://localhost:8080/JBCPPets/home.do), то вы получите ошибку.

Проблема заключается в нашем правиле URL перехвата. Для того, чтобы решить эту проблему нужно, чтобы пользователь имел ROLE_USER для доступа на любую страницу совпадающую с шаблоном URL /* - это означает, что правило сделано для всех страниц, включая и нашу страницу со входом.

Следующая диаграмма показывает, что происходит:

Spring Security

Мы должны обновить наши правила и позволить анонимным пользователям также заходить на сайт.

                        
<intercept-url pattern="/login.do" access="permitAll"/>
<intercept-url pattern="/*" access="hasRole('ROLE_USER')"/>
		

Внимание: использование выражений при задании правил может вызвать ошибку в пространстве имен и проект не запуститься. Ошибку можно исправить переключившись на Spring 3.02, хотя по официальным данным все должно работать и с такими настройками. Из выше сказанного я бы рекомендовал не использовать выражения, а идти "старым" путем. Вот, пример, который можно поставить если не работают выражения:

                        
<http auto-config="true">
  <intercept-url pattern="/login.do" filters="none" />
  <intercept-url pattern="/*" access="ROLE_USER"/>
  <form-login login-page="/login.do"/>
</http>
		

Попробуйте запустить приложение и убедиться, что все работает.


Понимание функцинальности logout

Logout - это термин для обозначения действия инициированного пользователем, результатом которого будет недействительность безопасной сессии. Обычно, пользователь после выхода перенаправляется в незащищенную часть сайта. Давайте добавим Log out ссылку в верхнюю часть сайта и рассмотрим, как он работает.

Добавление Log out ссылки в верхнюю часть сайта

Сигнатура URL для выхода пользователя - /j_spring_security_logout. Добавление этой ссылки в header.jsp не составит труда:

                        
<c:url value="/j_spring_security_logout" var="logoutUrl"/>
<li><a href="${logoutUrl}">Log Out</a></li>
		

Если вы перегрузите страницу и кликните на ссылку Log out, вы сразу переместитесь обратно к форме для входа.

Давайте взглянем, что же происходит за очень простой операцией выхода.

Как работает logout

Что в действительности происходит когда мы нажимаем Log Out ссылку?

Вы, наверное, помните, что каждый URL запрос идет через все фильтры цепочки Spring Security, перед тем как достигнет сервлета. URL запрос /j_spring_security_logout не соответствует какой-либо JSP странице в нашей системе. Этот тип URL часто называется, как виртуальный URL.

URL запрос /j_spring_security_logout перехватывается o.s.s.web.authentication.logout.LogoutFilter.

Давайте посмотрим на конфигурацию, которую предоставляет нам пространство имен security в отношении logout функциональности:

                        
<http auto-config="true" use-expressions="true">
  <logout invalidate-session="true"
    logout-success-url="/"
    logout-url="/j_spring_security_logout"/>
</http>
		

Выход пользователя из системы включает в себя три шага:

  1. Делает недействительной HTTP сессию (если invalidate-session установлена как true).
  2. Очищает SecurityContext
  3. Перенаправляет на URL указанный в logout-success-url.

Следующая диаграмма показывает, как происходит выход пользователя.

Spring Security

Реализация интерфейса o.s.s.web.authentication.logout.LogoutHandler может вызываться при выходе пользователя LogoutFilter. Это возможно (хотя сложно) реализовать свой LogoutHandler, который будет связан с жизненным циклом LogoutFilter. По умолчанию, LogoutHandler, который настраивается LogoutFilter, ответственен за очищение сессии и очищение функции remember me, так что сессия пользователя функционирует без аутентификационных связей. В конце, после того как выполнили logout, перенаправляемся на URL, который выполняется реализацией интерфейса o.s.s.web.authentication.logout.LogoutSuccessHandler. Эта реализация по умолчанию просто перенаправляет на заданный URL (по умолчанию, это "/"), но можно обновить эту реализацию для выполнения чего-нибудь еще, что требуется после того как пользователь вышел. Важно заметить, что logout обработчики не выкидывают исключений, так как считается важным выполнение всех действий.

Меняем URL для logout

Давайте поменяем поведение по умолчанию. Мы изменим URL на /logout:

                        
<http auto-config="true" use-expressions="true">
...
..<logout invalidate-session="true"
  logout-success-url="/"
  logout-url="/logout"/>
</http>
		

Также потребуется изменить /common/header.jsp файл:

                        
<c:url value="/logout" var="logoutUrl"/>
<li><a href="${logoutUrl}">Log Out</a></li>
		

Перезапустите приложение и попробуйте. Вы увидете, что вместо /j_spring_security_logout, используется /logout URL. Вы также можете заметить, что если наберете /j_spring_security_logout, то в результате получите ошибку Page not Found (404), так как URL не соответствует действительному ресурсу сервлета и он не больше будет обрабатываться фильтром.

Конфигурационные директивы logout

Элемент <logout> содержит дополнительные конфигурационные директивы, позволяющие делать функциональность выхода пользователя более сложной. Вот эти директивы:

Атрибут Описание
invalidate-session Если true, пользовательская HTTP сессия станет недействительной, когда будет logout. В некоторых случаях, это не желательно.
logout-success-url Это URL, на который будет перенаправлен пользователь когда произойдет logout. По умолчанию переходит на /. Это обрабатывается как HttpServletResponse.redirect.
logout-url Это URL, который читает LogoutFilter (мы его уже меняли)
success-handler-ref Бин ссылка на реализацию LogoutSuccessHandler.

Функция remember me - запомнить меня

Для пользователей часто посещающих сайт имеется удобная функция - remember me (запомнить меня). Эта особенность позволяет запоминать возвращающихся пользователей, через простые куки, зашифрованные и хранящиеся в браузере пользователя. Если Spring Security видит, что пользователь имеет куки remember me, то он автоматически входит в web приложение и ему не нужно вводить ни имени пользователя ни пароля.

В отличие от других особенностей, которые мы обсуждали ранее, функция запоминания не настраивается автоматически, когда мы используем пространство имен security. Давайте попробуем эту особенность и увидим, как это влияет на поток входа пользователя.

Реализация функции remember me

Завершение этого урока позволит обеспечить нам простую функциональность запоминания пользователя.

Давайте отредактируем dogstore-security.xml файл и добавим <remember-me>. Установим ключевой атрибут jbcpPetStore:

                        
<http auto-config="true" use-expressions="true" access-decision-
  manager-ref="affirmativeBased">
  ...
  <remember-me key="jbcpPetStore" />
  <logout invalidate-session="true" logout-success-url="/" logout-
    url="/logout" />
</http>
		

Если мы попробуем запустить наше приложение, то ничего не увидим. Это потому, что мы должны добавить поле в форму для входа, позволяющую пользователю активировать эту функциональность. Давайте отредактируем login.jsp файл и добавим checkbox похожий на следующий:

                        
<input id="j_username" name="j_username" size="20" maxlength="50" type="text" />
<br />
<input id="_spring_security_remember_me" 
name="_spring_security_remember_me" type="checkbox" value="true" />
<label for="_spring_security_remember_me">Remember Me?</label>
<br />
<label for="j_password">Password</label>:
		

Когда мы снова войдем в систему при включенном remember me, то соответствующие куки установятся в браузере пользователя.

Если пользователь закроет свой браузер и откроет его снова на странице для входа, ему не будет представлена страница для входа еще раз. Попробуйте сами – войдите с функцией remember me, перегрузите браузер и войдите на предыдущую страницу снова. Вы увидите, что немедленно войдете в систему без надобности ввода учетных данных.

Пользователи, которые хотят проверить эту функциональность могут также использовать плагин для браузера, который манипулирует (удаляет) куки сеанса, такой как Firecooke. Он поможет вам сохранить много времени при проверке такого рода функций.

Как работает функция remember me

Эта функция устанавливает куки на пользовательском браузере и содержит Base64 зашифрованную строку, которая состоит из таких частей:

Эти кусочки соединяются в одно куки значение, которое хранится браузером для дальнейшего использования.

MD5 – это один из нескольких известных алгоритмов криптографического хеширования. Этот алгоритм вычисляет компактное и уникальное текстовое представление вводимых данных с произвольной длинной, называемым digest. Этот digest может быть использован позже, чтобы проверить, что неизвестные вводимые данные точно совпадают с вводом использовавшимся при генерировании хеш, без необходимости наличия оригинального ввода. Следующая диаграмма показывает как это работает:

Spring Security

Как вы можете видеть, неизвестные введенные данные могут быть сверены с сохраненным MD5 хешем. Ключевое отличие между digest и алгоритмами шифрования, это то, что очень сложно сделать декомпиляцию оригинальных данных из digest значения. Это потому, что digest представляет из себя только, как бы, резюме оригинальных данных.

Хотя и нет возможности расшифровать зашифрованные данные, MD5 уязвим для нескольких типов атак, включая использование слабостей алгоритма и rainbow table attacks (атаки радужных таблиц). Радужные таблицы обычно содержат предварительно вычисленные хеши миллионов вводимых значений. Это позволяет злоумышленникам искать хеш значение в радужной таблице и определять фактическое (не хеш) значение. Позже мы рассмотрим методы борьбы с этим.

Угасание куки основывается на периоде истечения срока, длина которого настраивается.

В случае remember me куки, o.s.s.web.authentication.rememberme.RememberMeAuthenticationFilter включенный в цепочку фильтров, через объявление <remember-me>, будет знакомиться с содержанием куки и использовать его для аутентификации пользователя в том случае, если это окажется подлинный remember me куки.

Следующая диаграмма показывает различные компоненты вовлеченные в процесс проверки remember me куки:

Spring Security

RememberMeAuthenticationFilter вставляется в цепочку фильтров сразу же после SecurityContextHolderAwareRequestFilter, и сразу перед AnonymousProcessingFilter. Так же как и другие фильтры выполняются в цепочке, RememberMeAuthenticationFilter будет также проверять запрос, и если он представляет интерес, будут приняты соответствующие меры.

Как показывает диаграмма, фильтр отвечает за получение сведений, если браузер пользователя предоставил remember me куки, как часть их запроса. Если remember me куки был найден, он расшифровывается Base64, и MD5 хеш рассчитывается основываясь на имени пользователя и пароля представленных в куки. После того как куки проходят этот уровень проверки, пользователь может входить на сайт.

Заметка: Вы должны ожидать, что пользователь может изменить свое имя пользователя или пароль, любые remember me маркеры больше не будут в силе. Убедитесь, что вы предоставили соответствующее сообщение пользователю. Позже мы рассмотрим альтернативные remember me реализации, которые зависят только от имени пользователя, но не от пароля.

Мы увидим, что RememberMeAuthenticationFilter завист от реализации o.s.s.web.authentication.RememberMeServices для проверки куки. Таже реализация используется при удачном входе основанном на формах, только, если она видит параметры формы приходящие вместе с запросом входа с именем _spring_security_remember_me. Куки надежно зашифрованы с информацией описанной ранее, хранящейся в браузере в кодировке Base64 и содержащий MD5 хеш включая метку даты и пароля пользователя.

Remember me и жизненный цикл пользователя

Реализация RememberMeServices вызывается в нескольких точках жизненного цикла пользователя (жизненного цикла аутентификационного сеанса пользователей). В помощь для вашего понимания remember me функциональности, может быть полезно знать моменты времени, когда remember me сервисы были информированы о жизненном цикле функций:

Действие Что должно произойти?
Successful login Реализация устанавливает remember me куки (если был отправлен параметр формы).
Failed login Реализация должна отменить куки, если они установлены.
User logout Реализация должна отменить куки, если они установлены.

Зная, где и как RememberMeServices находится в связи с жизненным циклом пользователя, будет иметь важное значение, когда мы начнем создавать пользовательскую обработку аутентификации, потому что нам нужна гарантия, что любой аутентификационный процессор обрабатывает RememberMeServices последовательно, сохраняя полезность и безопасность данной функциональности.

Remember me конфигурационные директивы

Два изменения конфигурации, как правило, делаются для изменения поведения функциональности remember me по умолчанию:

Атрибут Описание
Key Определяет уникальный ключ для remember me куки связанного с нашим приложением.
token-validity-seconds Определяет длину времени (в секундах). Remember me куки будут считаться допустимыми для аутентификации. Также используется для установки истечения срока метки времени.

Как вы могли предположить из обсуждения о том, как содержание куки хешируются, key атрибут важен для защиты remember me функции. Будьте уверены, что ключ, который вы выбираете, скорее всего, будет уникальным для вашего приложения, и достаточно длинный, чтобы нельзя было легко угадать.

Remember me защищен?

Любая функция, связанная с безопасностью, которая добавленная для удобства пользователя, потенциально подвергается риску безопасности для нашего тщательно защищенного сайта. Функция remember me, в своей стандартной форме, подвергается риску перехвата куки пользователя и повторного использования злоумышленником. Следующая диаграмма показывает, как это может произойти:

Spring Security

Использование SSL (его мы разберем в другой части) и другие техники сетевой безопасности могут снизить этот тип атак, но имейте ввиду, что есть и другие техники такие как cross-site scripting (XSS), которые могут украсть или подвергнуть риску запомнившийся сеанс пользователя.

Один общий подход для поддержания баланса между удобством и безопасностью – это определение функциональных мест на web сайте, где может быть представлена персональная информация. Гарантировать защищенность этих место можно используя авторизацию, которая проверяет не только роль пользователя, но и то, что пользователь прошел аутентификацию с указанием имени пользователя и пароля. Этого можно достигнуть, используя псевдо-свойство fullyAuthenticated, поддерживаемое SpEL выражениями для привил авторизации, которые мы описали в первой части.

Правила авторизации различающие запомнивших (remember me) и полностью аутентификационные сеансы

Мы будем рассматривать современную авторизацию в следующей части, тем не менее, важно сделать все возможное для различия прав доступа в зависимости от подлинности сеанса, который был запомнен.

Предположим, что пользователь может посмотреть и отредактировать "wish list" на сайте тогда, когда он зайдет с запомнившимся сеансом (remember me). Это похоже на поведение коммерческих сайтов, и не представляет угрозы персональной информации (имейте ввиду, что каждый сайт по своему уникален и не стоит слепо следовать этим инструкциям). Вместо этого, мы сконцентрируемся на защите учетной записи пользователя. Мы хотим быть уверенными, что пользователям, которые хотят получить доступ к информации об учетной записи, требовалось бы аутентифицировать себя. Здесь мы настроим правила авторизации:

                        
<intercept-url pattern="/login.do" access="permitAll"/>
<intercept-url pattern="/account/*.do" access="hasRole('ROLE_USER') and fullyAuthenticated"/>
<intercept-url pattern="/*" access="hasRole('ROLE_USER')"/>
		

Правила для страницы входа и ROLE_USER остаются неизменными. Мы добавили правило, которое требует, чтобы запросы по информации об учетной записи имели соответствующий GrantedAuthority ROLE_USER, и, что пользователь должен пройти полную аутентификацию, то есть, в ходе этого аутентификационного сеанса, пользователь действительно должен предоставить имя пользователя и пароль или другие полномочия. Обратите внимание на синтаксис логических операций SpEL – and, or и not.

Имейте в виду, если вы попытаетесь получить доступ к ссылке My Account после использования remember me для входа, то получите сообщение 403 Access Forbidden. Это происходит потому, что приложение все еще настроено со стандартным AccessDeniedHandler, который ответственен за захват и ответы в AccessDeniedException отчеты. Мы изменим это поведение в других частях, когда будем изучать как работает AccessDeniedException.

Заметка: Если ваше приложение не использует SpEL выражения для объявления доступа, вы можете сделать это с помощью провила IS_AUTHENTICATED_FULLY (например, access=" IS_AUTHENTICATED_FULLY"). Однако, имейте ввиду, что стандартные правила объявления ролей доступа не такие выразительные как SpEL, так вы будете иметь проблемы при обработке нескольких булевских выражений.


Строим IP remember me сервис (RememberMeService)

Первый способ сделать remember me более защищенным - использовать привязку IP адреса пользователя с содержанием куки. Давайте посмотрим пример того, как мы будем строить нашу собственную реализацию RememberMeServices.

Основной подход для реализации, это расширение базового класса o.s.s.web.authentication.rememberme.TokenBasedRememberMeServices и это расширение позволяет добавить IP адрес заявителя в куки и в MD5 хеш.

Расширение базового класса будет включать замещение двух ключевых методов, и замещение или реализацию очень незначительных вспомогательных методов. Также мы должны будем временно хранить HttpServletRequest (который мы используем для получения IP адреса) в ThreadLocal, так как методы базового класса не принимают HttpServletRequest, как параметр.

Расширение TokenBasedRememberMeServices

Сперва, мы должны расширить TokenBasedRememberMeServices класс и заместить определенное поведение родителя. Создадим класс в com.packtpub.springsecurity.security пакете:

                        
public class IPTokenBasedRememberMeServices extends
  TokenBasedRememberMeServices {
		

Некоторые простые методы используются для установки и получения ThreadLocal HttpServletRequest:

                        
private static final ThreadLocal<HttpServletRequest> requestHolder =
new ThreadLocal<HttpServletRequest>();

public HttpServletRequest getContext() {
  return requestHolder.get();
}

public void setContext(HttpServletRequest context) {
  requestHolder.set(context);
}
		

Мы также добавим полезный метод для получения IP адреса из HttpServletRequest:

                        
protected String getUserIPAddress(HttpServletRequest request) {
  return request.getRemoteAddr();
}
		

Первый интересный метод, который мы будем замещать - это onLoginSuccess, который используется для установки значения куки для remember me процессора. В этом методе, нам нужно установить ThreadLocal и очистить его после окончания. Запомните, что поток родительских методов, агрегирует всю информацию о запросе аутентифицированного пользователя и синтезирует ее в куки.

                        
@Override
public void onLoginSuccess(HttpServletRequest request,
  HttpServletResponse response,
  Authentication successfulAuthentication) {
	
  try
  {
    setContext(request);
    super.onLoginSuccess(request, response, successfulAuthentication);
  }
	
  finally
  {
    setContext(null);
  }
}
		

Метод родительского класса onLoginSuccess вызывает makeTokenSignature, используемый для создания MD5 хеша аутентификационных учетных данных. Мы будем замещать его для получения IP адреса из запроса, и шифровать возвращенный куки используя полезный класс из Spring Framework:

                        
@Override
protected String makeTokenSignature(long tokenExpiryTime,
String username, String password) {
  return DigestUtils.md5DigestAsHex((username + ":" +
  tokenExpiryTime + ":" + password + ":" + getKey() + ":" + 
  getUserIPAddress(getContext())).getBytes());
}
		

Подобным образом мы будем замещать метод setCookie для добавления дополнительного шифрования, которое включает запрашиваемый IP адрес:

                        
@Override
protected void setCookie(String[] tokens, int maxAge,
  HttpServletRequest request, HttpServletResponse response) {
  // append the IP adddress to the cookie
  String[] tokensWithIPAddress =
    Arrays.copyOf(tokens, tokens.length+1);
  tokensWithIPAddress[tokensWithIPAddress.length-1] =
    getUserIPAddress(request);
  super.setCookie(tokensWithIPAddress, maxAge,
    request, response);
}
		

Это дает нам все кусочки для того, чтобы создать новый куки!

В конце мы заместим processAutoLoginCookie метод, который используется для проверки содержания remember me куки, которые предоставил пользователь. Суперкласс делает большинство интересной для нас работы. Теперь давайте сделаем проверку IP адреса перед вызовом родителя.

                        
@Override
protected UserDetails processAutoLoginCookie(
  String[] cookieTokens,
  HttpServletRequest request, HttpServletResponse response) {
  try
  {
      setContext(request);
      // take off the last token
      String ipAddressToken =
        cookieTokens[cookieTokens.length-1];
		
      if(!getUserIPAddress(request).equals(ipAddressToken))
      {
        throw new InvalidCookieException("Cookie IP Address did not
contain a matching IP (contained '" + ipAddressToken + "')");
      }
	
      return super.processAutoLoginCookie(Arrays.copyOf(cookieTokens,
cookieTokens.length-1), request, response);
  }
	
  finally
  {
    setContext(null);
  }
}
		

Наш код для пользовательского RememberMeServices готов! Сейчас мы сделаем небольшие настройки.

Настройка пользовательского RememberMeServices

Настройка пользовательской реализации RememberMeServices требует два шага. Первый – редактирование конфигурационного файла Spring dogstore-base.xml. В него мы добавим новый бин:

<bean class="com.packtpub.springsecurity.security.
IPTokenBasedRememberMeServices" id="ipTokenBasedRememberMeServicesBean">
  <property name="key"><value>jbcpPetStore</value></property>
  <property name="userDetailsService" ref="userService"/>
</bean>
		

Вторая незначительная настройка касается Spring Security XML конфигурационного файла. Отредактируйте <remember-me> элемент для создания ссылки на ваш пользовательский Spring Bean:

<remember-me key="jbcpPetStore"
  services-ref="ipTokenBasedRememberMeServicesBean"/>
		

В конце, добавьте id атрибут к <user-service>:

<user-service
  id="userService">
		

Перегрузите web приложение и вы увидите как работает новая IP фильтрация!

Remember me куки зашифрованы с Base64, но мы можем проверить наши измения в коде путем извлечения значения куки и его расшифровки, используя Base64 средство декодирования. Когда мы декодируем, то увидим куки с именем SPRING_SECURITY_REMEMBER_ME_COOKIE и с содержанием похожим на следующее:

guest:1251695034322:776f8ad44034f77d13218a5c431b7b34:127.0.0.1

Вы можете увидеть IP адрес прямо в конце куки, как мы и ожидали. Вы также видите имя пользователя, временную метку, MD5 хеш, соответственно.


Изменение сигнатуры remember me

Некоторые пользователи могут удивиться тому, что ожидаемое значение поля формы checkbox у remember me, _spring_security_remember_me, или имя куки SPRING_SECURITY_REMEMBER_ME_COOKIE, может меняться. Пока <remember-me> объявление не обладает этой гибкостью, но сейчас мы будем объявлять свою реализацию RememberMeServices как Spring Bean. Мы можем просто определить еще больше свойств для изменения имени checkbox и имени куки:

<bean class="com.packtpub.springsecurity.web.custom.
IPTokenBasedRememberMeServices" id="ipTokenBasedRememberMeServicesBean">
  <property name="key"><value>jbcpPetStore</value></property>
  <property name="userDetailsService" ref="userService"/>
  <property name="parameter" value="_remember_me"/>
  <property name="cookieName" value="REMEMBER_ME"/>
</bean>
		

Не забудьте изменить страницу login.jsp, установив имя checkbox.


Реализация управления сменой пароля

Сейчас мы рассмотрим in-memory UserDetailsService, позволяющее пользователю менять свой пароль. Хотя эта функция полезна, когда имена пользователей и пароли хранятся в базе данных, но мы реализуем это при помощи расширения o.s.s.core.userdetails.memory.InMemoryDaoImpl, которое позволит нам сосредоточиться не на механизмах хранения, а на общем потоке и дизайне этого типа расширения. Позже, в следующей части, мы расширим базовую часть через использование базы данных.

Расширение хранения учетных данных in-memory для поддержки смены пароля

InMemoryDaoImpl in-memory хранение учетных данных поставляется с Spring Security фреймворком, здесь используется простая карта для хранения имен пользователей и их связанные UserDetails. Реализация UserDetails используемая InMemoryDaoImpl - это o.s.s.core.userdetails.User.

Дизайн этого расширения намеренно сделан простым и скрывает важные детали, такие как требование к пользователю поставлять их старый пароль до его изменения. Добавление этих функций мы оставим как упражнения для читателя.

Расширение InMemoryDaoImpl с InMemoryChangePasswordDaoImpl

Сперва мы напишем свой пользовательский класс для расширения базового InMemoryDaoImpl, и добавим метод, позволяющий нам менять пароль пользователя. User - неизменный объект, мы должны будем скопировать, и изменить пароль. Давайте объявим интерфейс, который иллюстрирует метод, позволяющий изменять пароль:

package com.packtpub.springsecurity.security;
// imports omitted
public interface IChangePassword extends UserDetailsService { void
  changePassword(String username, String password);
}
		

Следующий код позволяет менять пароль в in-memory хранилище данных:

public class InMemoryChangePasswordDaoImpl extends InMemoryDaoImpl implements
        IChangePassword {
    @Override
    public void changePassword(String username, String password) {
    
        // get the UserDetails
        User userDetails = (User) getUserMap().getUser(username);
        
        // create a new UserDetails with the new password
        User newUserDetails = new User(userDetails.getUsername(), password,
                userDetails.isEnabled(), userDetails.isAccountNonExpired(),
                userDetails.isCredentialsNonExpired(), userDetails
                        .isAccountNonLocked(), userDetails.getAuthorities());
                        
        // add to the map
        getUserMap().addUser(newUserDetails);
    }
}
		

К счастью, не так много требуется, чтобы добавить эту простую функциональность к нашему подклассу. Давайте рассмотрим требования, которые нужны для добавления пользовательского UserDetailsService к нашему сайту.

Настройка Spring Security для использования InMemoryChangePasswordDaoImpl

Сейчас нам нужно перенастроить Spring Security XML конфигурационный файл для использования нашего новой UserDetailsService реализации. К сожалению, это немного сложно, чем мы думали, так <user-service> элемент берет специальную обработку в Spring Security конфигурационном процессоре. Вместо этого, мы явно объявим свой бин и удалим <user-service> элемент, который мы объявляли. Давайте изменим:

<authentication-manager alias="authenticationManager">
  <authentication-provider>
    <user-service id="userService">
      <user authorities="ROLE_USER" name="guest" password="guest" />
    </user-service>
  </authentication-provider>
</authentication-manager>
		

На:

<authentication-provider user-service-ref="userService"/>
		

Атрибут user-service-ref, как мы можем предположить, это ссылка к Spring Bean с id userService. Так, в нашем dogstore-base.xml Spring Beans XML конфигурационном файле, мы объявили следующий бин:

<bean id="userService"
  class="com.packtpub.springsecurity.security.
InMemoryChangePasswordDaoImpl">
  <property name="userProperties">
    <props>
      <prop key="guest">guest,ROLE_USER</prop>
    </props>
  </property>
</bean>
		

Вы можете заметить, что синтаксис для объявления пользователя не читается как <user> элемент содержащий <user-service> объявление. К сожалению, <user> элемент доступен только когда используем реализацию InMemoryDaoImpl по умолчанию, и не может использоваться с пользовательским UserDetailsService. Для целей нашего примера, это ограничение делает вещи немного сложнее, но в реальности, хранение пользовательских определений в конфигурационном файле не совсем хорошая идея. Для тех кому интересно, в раздел 6.2 документации Spring Security 3 описывает все детали декларативного синтаксиса для предоставления информации о пользователе.

Строим страницу для смены пароля

Мы будем строить простую страницу для смены пароля.

Эта страница связана с My Account страницей через простую ссылку. Первым делом мы добавим ссылку /account/home.jsp:

<p>
  Please find account functions below...
</p>
<ul>
  <li><a href="changePassword.do">Change Password</a></li>
</ul>
		

Далее, мы построим страницу Change Password (/account/changePassword.jsp):

<?xml version="1.0" encoding="ISO-8859-1" ?>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
  pageEncoding="ISO-8859-1"%>
<jsp:include page="../common/header.jsp">
  <jsp:param name="pageTitle" value="Change Password" />
</jsp:include>
<h1>Change Password</h1>
<form method="post"><label for="password">New Password</label>: <input
  id="password" name="password" size="20" maxlength="50" type="password" />
<br />
<input type="submit" value="Change Password" /></form>
<jsp:include page="../common/footer.jsp" />
		

В конце, мы должны прибавить наш Spring MVC AccountController для обработки отправки запроса смены пароля (мы не рассматривали AccountController в предыдущих главах).

Добавляем обработчик смены пароля к AccountController

Нам нужно добавить инъекцию-ссылку на наш пользовательский UserDetailsService в com.packtpub.springsecurity.web.controller.AccountController, так, чтобы сделать функционирование смены пароля возможным. Spring @Autowired аннотация делает это очень просто:

@Autowired
private IChangePassword changePasswordDao;
		

Два метода обрабатывающих запросы будут беспокоится за визуализацию формы, и обработку отправленных данных через POST:

@RequestMapping(value="/account/changePassword.do",method=RequestMethod.GET)
public void showChangePasswordPage() {		
}

@RequestMapping(value="/account/changePassword.do",method=RequestMethod.POST)
public String submitChangePasswordPage(@RequestParam("password")
String newPassword) {
  Object principal = SecurityContextHolder.getContext().
  getAuthentication().getPrincipal();
  String username = principal.toString();
	
  if (principal instanceof UserDetails) {
    username = ((UserDetails)principal).getUsername();
  }
	
  changePasswordDao.changePassword(username, newPassword);
  SecurityContextHolder.clearContext();
  return "redirect:home.do";
}
		

Все, можно пробовать!