SPRING-SOURCE.RU | |
|
|
Добравшись до этой части, мы уже имеем некоторые улучшения на нашем сайте: пользовательская форма входа, функция смены пароля и функция remember me.
В этой части мы пойдем в сторону использования базы данных в качестве хранилища для аутентификации. Мы будем изучать Spring Security schema базы данных, и будем искать пути чтобы расширить JDBC реализацию.
В ходе изучения этой части мы познакомимся:
Очевидным вопросом на счет нашей безопасности является то, что хранение в памяти пользователей и паролей имеет очень непродолжительное время жизни. Как только приложение перегружается, любые новые регистрации пользователей или любые другие действия будут потеряны. Это неприемлемо, поэтому следующим логическим шагом будет переконфигурирование Spring Security для использования базы данных в качестве хранилища аутентификационных данных. Использование JDBC реляционных баз данных позволит пользовательским данным сохраняться даже, если сервер приложения перегрузился. Это является более типичным отражением Spring Security в реальном мире.
Первая часть этого упражнения включает создание, основанной на Java, реляционной базы данных HyperSQL DB (HSQL). Мы будем настраивать HSQL для запуска в режиме in-memory.
Мы предоставляем SQL файл, security-schema.sql, который будет создавать все таблицы необходимые для реализации Spring Security, используя HSQL. Если вы используете свою базу данных, то подкорректируйте этот файл. Мы кладем этот файл в WEB-INF/classes.
Для настройки HSQL мы будем редактировать dogstore-security.xml файл. В нем мы настроим базу данных и запуск SQL для создания структуры таблиц Spring Security. Сперва, мы добавим ссылку к jdbc XML schema в начале файла:
<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" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security3.0.xsd">
Далее, мы объявим <embedded-database> элемент, наряду со ссылкой на SQL скрипт:
<jdbc:embedded-database id="dataSource" type="HSQL"> <jdbc:script location="classpath:security-schema.sql"/> </jdbc:embedded-database>
Если вы сейчас запустите сервер, вы увидите в отчетах, как проходит иницилизация HSQL. Помните, что объявление <embedded-database> создает базу данных только в памяти.
Мы редактировали dogstore-security.xml файл чтобы указать, что мы используем JDBC UserDetailsService реализацию, вместо Spring Security in-memory UserDetailsService, который мы настраивали в предыдущих частях. Это достигается за счет простого объявления <authentication-manager>:
<authentication-manager alias="authenticationManager"> <authentication-provider> <jdbc-user-service data-source-ref="dataSource"/> </authentication-provider> </authentication-manager>
В конце, мы создаем другой SQL файл, который будет запущен, когда будет создана база данных в памяти. Этот SQL файл будет содержать информацию о наших стандартных пользователях, admin и guest, с теми же GrantedAuthority настройками, которые мы использовали в предыдущей части. Файл, который мы будем вызывать будет иметь название test-data.sql. Мы положим его вместе с security-schema.sql в WEB-INF/classes:
insert into users(username, password, enabled) values ('admin','admin',true); insert into authorities(username,authority) values ('admin','ROLE_USER'); insert into authorities(username,authority) values ('admin','ROLE_ADMIN'); insert into users(username, password, enabled) values ('guest','guest',true); insert into authorities(username,authority) values ('guest','ROLE_USER'); commit;
Далее, нам нужно добавить этот SQL файл к конфигурации базы данных, так, чтобы он загружался на старте:
<jdbc:embedded-database id="dataSource" type="HSQL"> <jdbc:script location="classpath:security-schema.sql"/> <jdbc:script location="classpath:test-data.sql"/> </jdbc:embedded-database>
Сейчас, при запуске приложения, Spring Security будет обращаться к базе данных для аутентификации и GrantedAuthority информации!
Вы можете вспомнить, когда мы рассматривали аутентификационный процесс во второй части, что AuthenticationManager делегирует AuthenticationProvider подтвердить учетные данные принципала и гарантировать, что он будет иметь доступ к системе. AuthenticationProvider, который мы использовали во второй и третьей части был DaoAuthenticationProvider. Этот провайдер делегирует реализации UserDetailsService получить и подтвердить информацию о принципале из хранилища учетных данных. Мы могли увидеть это из диаграммы во второй части:
Отличия между нашей конфигурацией базы данных как хранилища аутентификационных данных и хранения в памяти, составляет реализация UserDetailsService. Класс o.s.s.core.userdetails.jdbc.JdbcDaoImpl предоставляет реализацию UserDetailsService. Вместо того, чтобы обращаться в хранилище в памяти, JdbcDaoImpl смотрит пользователя в базе данных.
Важно заметить, что объявление <jdbc-user-service> в обновленной конфигурации будет автоматически настраивать JdbcDaoImpl и связывать его с AuthenticationProvider. Позже в этой части мы увидим, как настраивать Spring Security для использования своей реализации JdbcDaoImpl, с поддержкой функции смены пароля, которую мы добавили в пользовательском InMemoryDaoImpl. Давайте рассмотрим конфигурацию необходимую для реализации своего подкласса JdbcDaoImpl, который поддерживает функцию смены пароля.
В одном из упражнений в предыдущей части, мы сделали JdbcDaoImpl, как стартовую точку, теперь мы расширим его для поддержки функции смены пароля.
Класс будет находиться в пакете com.packtpub.springsecurity.security:
public class CustomJdbcDaoImpl extends JdbcDaoImpl implements IChangePassword { public void changePassword(String username, String password) { getJdbcTemplate().update("UPDATE USERS SET PASSWORD = ? WHERE USERNAME = ?", password, username); } }
Вы можете видеть, что этот простой класс расширяет стандартный JdbcDaoImpl с функцией обновления пароля в базе данных на новый пароль. Здесь мы используем стандартную Spring JDBC функциональность.
Добавим следующее объявление Spring Bean в dogstore-base.xml в конфигурационном файле Spring:
<bean id="jdbcUserService" class="com.packtpub.springsecurity.security.CustomJdbcDaoImpl"> <property name="dataSource" ref="dataSource"/> </bean>
Мы с вами расширяли JdbcDaoImpl, при этом сохраняли базовую функциональность. Но, что, если мы хотим реализовать более продвинутые функции, такие как регистрация и управление пользователями, которые позволяют администратору сайта создавать пользователей, обновлять пароли и так далее?
Хотя эти типы функций относительно простые, Spring Security предоставляет функциональность "из коробки" для поддержки общих Create, Read, Update и Delete (CRUD) операций в JDBC базах данных. Это может быть удобно для простых систем, и хорошая основа.
Реализация o.s.s.provisioning.JdbcUserDetailsManager просто расширяет для нас JdbcDaoImpl, и предоставляет несколько полезных методов, объявленных, как часть o.s.s.provisioning.UserDetailsManager интерфейса:
Метод | Описание |
void createUser(UserDetails user) | Создает нового пользователя с заданным UserDetails, включая любой объявленный GrantedAuthority. |
void updateUser(final UserDetails user) | Обновляет пользователя с заданным UserDetails. Обновляет GrantedAuthority и удаляет пользователя из пользовательского кеша. |
void deleteUser(String username) | Удаляет пользователя с заданным именем пользователя, и удаляет пользователя из пользовательского кеша. |
boolean userExists(String username) | Показывает, есть или нет пользователь (активный или нет) существующий с заданным именем пользователя. |
void changePassword(String oldPassword, String newPassword) | Изменяет пароль текущего, вошедшего в систему пользователя. Пользователь должен предоставить правильный текущий пароль для того, чтобы операция была успешной. |
Как вы можете увидеть, метод changePassword в JdbcUserDetailsManager закрывает разрыв в функциональности нашего CustomJdbcDaoImpl класса - при смене пароля на новый, он будет проверять текущий пароль пользователя. Давайте рассмотрим шаги необходимые для того, чтобы заменить наш CustomJdbcDaoImpl новым JdbcUserDetailsManager.
Первым делом, нам нужно объявить бин JdbcUserDetailsManager в dogstore-base.xml файле:
<bean id="jdbcUserService" class="org.springframework.security .provisioning.JdbcUserDetailsManager"> <property name="dataSource" ref="dataSource" /> <property name="authenticationManager" ref="authenticationManager" /> </bean>
Ссылка на AuthenticationManager совпадает с alias, который мы недавно объявляли в элементе <authentication-manager> в dogstore-security.xml файле. Незабудьте закомментировать бин CustomJdbcDaoImpl - мы (временно) не будем его использовать.
Далее, мы сделаем небольшие изменения в файле changePassword.jsp:
<h1>Change Password</h1> <form method="post"> <label for="oldpassword">Old Password</label>: <input id="oldpassword" name="oldpassword" size="20" maxlength="50" type="password"/> <br /> <label for="password">New Password</label>: <input id="password" name="password" size="20" maxlength="50" type="password"/> <br />
В конце, мы сделаем небольшие изменения в AccountController. Заменим ссылку @Autowired на IChangePassword реализацию:
@Autowired private UserDetailsManager userDetailsManager;
submitChangePasswordPage также будет очень простой:
public String submitChangePasswordPage(@RequestParam("oldpassword") String oldPassword, @RequestParam("password") String newPassword) { userDetailsManager.changePassword(oldPassword, newPassword); SecurityContextHolder.clearContext(); return "redirect:home.do"; }
После завершения всех этих действий, вы можете перезапустить ваше web приложение и попробовать новую функциональность смены пароля.
Обратите внимание на то, что происходит, когда вы передаете неправильный пароль. Как вы думаете, почему это происходит? Подумайте над тем, как сделать все это более удобным для пользователя.
Хотя мы не рассмотрели полную функциональность JdbcUserDetailsManager, мы можем видеть, что очень просто связывать JSP страницы (защищенные авторизацией, конечно!), позволяя администраторам управлять пользователями.
JdbcDaoImpl имеет несколько опций, которые позволяют адаптировать его к использованию в существующей schema, или делать более сложные изменения в существующих возможностях. Во многих случаях можно адаптировать JDBC UserDetailsService прямо, что называется, "из коробки", без использования дополнительного кода.
Одна важная особенность - способность добавлять уровни скрытности (косвенности - indirection) между пользователями и объявлениями GrantedAuthority, группируя GrantedAuthority в логические наборы, называемыми groups - группы.
Как видно на диаграмме, эта косвенность (indirection) позволяет назначать тот же набор ролей к нескольким пользователям, просто добавляя нового пользователя в существующую группу.
Это объединение общих наборов полномочий может быть полезно в следующих случаях:
Если ваше приложение имеет очень маленькую базу пользователей, высока вероятность, что вы будете использовать контроль доступа основанный на группах.
Мы добавили две группы к сайту - обычные пользователи, которых мы называем "Users", и администраторы, которых мы называем "Administrators". Существующие guest и admin учетные записи пользователей будут брошены в подходящие группы через изменения в нашем SQL скрипте, который мы использовали для создания базы данных.
Первым делом, мы должны установить свойства на нашем пользовательском подклассе JdbcDaoImpl для включения использования групп, и выключения использования прямого предоставления полномочий пользователям. Добавим это в следующее определение бина в dogstore-base.xml
<bean id="jdbcUserService" class="com.packtpub.springsecurity.security.CustomJdbcDaoImpl"> <property name="dataSource" ref="dataSource"/> <property name="enableGroups" value="true"/> <property name="enableAuthorities" value="false"/> </bean>
Мы просто будем изменять наш SQL, который мы используем для заполнения базы данных:
Для простоты мы создадим новый SQL скрипт под названием test-users-groups-data.sql.
Первым делом мы добавим группы:
insert into groups(group_name) values ('Users'); insert into groups(group_name) values ('Administrators');
Далее, назначим роли группам
insert into group_authorities(group_id, authority) select id,'ROLE_ USER' from groups where group_name='Users'; insert into group_authorities(group_id, authority) select id,'ROLE_ USER' from groups where group_name='Administrators'; insert into group_authorities(group_id, authority) select id,'ROLE_ ADMIN' from groups where group_name='Administrators';
Далее, создадим пользователей:
insert into users(username, password, enabled) values ('admin','admin',true); insert into users(username, password, enabled) values s('guest','guest',true);
В конце, назначим пользователей группам
insert into group_members(group_id, username) select id,'guest' from groups where group_name='Users'; insert into group_members(group_id, username) select id,'admin' from groups where group_name='Administrators';
Мы должны обновить создание HSQL базы данных, поменяв скрипт:
<jdbc:embedded-database id="dataSource" type="HSQL"> <jdbc:script location="classpath:security-schema.sql"/> <jdbc:script location="classpath:test-users-groups-data.sql"/> </jdbc:embedded-database>
Если мы сейчас запустим наше приложение, то оно будет вести себя также, как и раньше. Тем не менее, дополнительный слой абстракции между пользователями и привилегиями, даст нам возможность разрабатывать более сложные функциональности по управлению пользователями (далее).
Давайте, в данный момент, посмотрим на важный кусочек конфигурации.
Как правило, новые пользователи Spring Security начинают свой опыт через адаптирование JDBC пользователя, группы или роли к существующей schema. Хотя существующая база данных не соответствует существующей schema Spring Security. Мы можем настроить JdbcDaoImpl для этого соотношения.
Представьте, что существующая schema базы данных похожа на следующую диграмму:
Мы можем просто изменить конфигурацию JdbcDaoImpl для использования этой schema и заменить ожидаемые определения таблиц Spring Security и колонок, которые мы используем для нашего сайта.
JdbcDaoImpl имеет три SQL запроса, которые имеют четко определенные параметры и набор возращенных колонок. Мы должны определить SQL, который мы назначим каждому из этих запросов. Каждый SQL запрос, использующийся JdbcDaoImpl, берет имя пользователя представленное для входа в систему как единственный параметр.
Имя запроса | Описание | Ожидаемая SQL колонка |
usersByUsernameQuery | Возвращает одного или более пользователей совпадающих с именем пользователя. |
Username (string) Password (string) Enabled (Boolean) |
authoritiesByUsernameQuery | Возвращает один или более granted authorities (предоставленные полномочия), напрямую предоставляемые пользователю. Обычно используется, когда GBAC выключен. |
Username (string) Granted Authority (string) |
groupAuthoritiesByUsernameQuery | Возвращает подтвержденные полномочия и детали группы предоставляемые пользователю через group membership (членство в группе). Используется, когда GBAC включен. |
Group Primary Key (any) Group Name (any) Granted Authority (string) |
Будьте внимательны, в некоторых случаях, возвращенная колонка не используется по умолчанию реализацией JdbcDaoImpl, но они должны быть возвращены в любом случае. Прежде чем двигаться дальше давайте попробуем написать эти запросы для диаграммы к базе данных, которую мы нарисовали ранее.
В случае использования пользовательских SQL запросов для нашей нестандартной schema, мы просто настраиваем свойства JdbcDaoImpl в Spring Bean конфигурационном файле. Имейте ввиду, что в случае настройки JDBC запросов JdbcDaoImpl, вы не можете использовать <jdbc-user-service> объявление.
<bean id="jdbcUserService" class="com.packtpub.springsecurity.security.CustomJdbcDaoImpl"> <property name="dataSource" ref="dataSource" /> <property name="enableGroups" value="true" /> <property name="enableAuthorities" value="false" /> <property name="usersByUsernameQuery"> <value>SELECT LOGIN, PASSWORD, 1 FROM USER_INFO WHERE LOGIN = ? </value> </property> <property name="groupAuthoritiesByUsernameQuery"> <value>SELECT G.GROUP_ID, G.GROUP_NAME, P.NAME FROM USER_INFO U JOIN USER_GROUP UG on U.USER_INFO_ID = UG.USER_INFO_ID JOIN GROUP G ON UG.GROUP_ID = G.GROUP_ID JOIN GROUP_PERMISSION GP ON G.GROUP_ID = GP.GROUP_ID JOIN PERMISSION P ON GP.PERMISSION_ID = P.PERMISSION_ID WHERE U.LOGIN = ? </value> </property> </bean>
Эта конфигурация необходима для использования Spring Security для чтения настроек из существующей, нестандартной schema! Запомните, что использование существующей schema обычно требует расширения JdbcDaoImpl для поддержки смены пароля, переименования учетной записи пользователя, и других функций управления пользователем.
Если вы используете JdbcUserDetailsManager для выполнения задач управления пользователем, помните, что только некторые из двадцати SQL запросов выполняемые классом, доступны через конфигурацию. За дополнительной информацией обращайтесь к Javadoc.
Следующие главные правила должны применяться к паролям хранящимся в базе данных:
Обычно, bootstrap процесс (заполнение системы начальными пользователями и данными) обрабатывается через некоторое сочетание SQL загрузок и Java кода. Все зависит от сложности приложения.
Для нашего приложения, мы сохраним объявление базы данных и соответствующие SQL. Затем добавим немного Java для запуска, после начальной загрузки, шифрования всех паролей в базе данных. Для правильного шифрования пароля два субъекта должны использовать шифрованный пароль в синхронизации, гарантируя, что пароли обрабатываются и проверяются последовательно.
Шифрование пароля в Spring Security инкапсулируется и определяется реализацией интерфейса o.s.s.authentication.encoding.PasswordEncoder. Простая настройка шифрования пароля возможна через объявление <password-encoder> с элементом <authentication-provider>:
<authentication-manager alias="authenticationManager"> <authentication-provider user-service-ref="jdbcUserService"> <password-encoder hash="sha"/> </authentication-provider> </authentication-manager>
Spring Security поставляется с рядом PasswordEncoder реализаций, которые применимы для различных нужд и требований безопасности. Реализация, которая используется, может быть задана через атрибут hash у <password-encoder> объявления.
Следующая таблица предлагает список реализаций классов "из коробки" и описывает их выгоды. Заметьте, что все реализации находятся в o.s.s.authentication.encoding пакете.
Реализация класса | Описание | Значение hash |
PlaintextPasswordEncoder | Шифрует пароль как обычный текст. По умолчанию шифрование пароля DaoAuthenticationProvider. | plaintext |
Md4PasswordEncoder | PasswordEncoder использует MD4 хеш алгоритм. MD4 - это не алгоритм шифрования. Не рекоммендуется использовать в качестве кодировщика. | md4 |
Md5PasswordEncoder | PasswordEncoder использует MD5 алгоритм шифрования. | md5 |
ShaPasswordEncoder | PasswordEncoder использует SHA алгоритм шифрования. Этот шифровальщик может поддерживать настраиваемые уровни силы шифрования. |
sha sha-256 |
LdapShaPasswordEncoder | LDAP SHA и LDAP SSHA реализации алгоритмов используются для интеграции с аутентификационным хранилищем LDAP. |
{sha} {ssha} |
Как и во многих других областях в Spring Security, есть возможность ссылаться на определение бина реализации PasswordEncoder, предоставляя более точную настройку и позволяя связывать PasswordEncoder с другими бинами через инъекцию зависимостей. Для нашего сайта, нужно будет использовать ссылочный метод бина для шифрования bootstrap пользовательских данных.
Давайте рассмотрим процесс настройки шифрования пароля для нашего приложения.
Настройка базового шифрования пароля включает в себя два момента - шифрование паролей, которые мы загрузили в базу данных после запуска SQL скрипта, и гарантирование того, что DaoAuthenticationProvider настроен для работы с PasswordEncoder.
Первым делом мы объявим экземпляр PasswordEncoder как Spring бин:
<bean class="org.springframework.security.authentication. encoding.ShaPasswordEncoder" id="passwordEncoder"/>
Вы могли заметить, что мы использовали SHA-1 PasswordEncoder реализацию. Это эффективный алгоритм шифрования, часто используемый для хранения паролей.
Нам нужно настроить DaoAuthenticationProvider для того, чтобы иметь ссылку на PasswordEncoder, так чтобы он мог шифровать и сравнивать пароль представленный при входе пользователя в систему. Просто добавим <password-encoder> объявление и передадим в качестве ссылки, ID бина, объявленного ранее.
<authentication-manager alias="authenticationManager"> <authentication-provider user-service-ref="jdbcUserService"> <password-encoder ref="passwordEncoder"/> </authentication-provider> </authentication-manager>
Попробуйте, в данный момент, запустить приложение и зайти в систему. Вы заметите, что предыдущий удачный вход в систему теперь не работает. Это происходит потому, что пароль храниться в базе данных (загружается с bootstrap test-users-groups-data.sql скриптом) хранится в не зашифрованной форме. Мы должны сделать последующую обработку данных bootstrap простым Java кодом.
Подход, который мы будем использовать для шифрования паролей загруженных через базу данных, использует Spring бин, который запускает начальный метод после иницилизации embedded-database бина:
public class DatabasePasswordSecurerBean extends JdbcDaoSupport { @Autowired private PasswordEncoder passwordEncoder; public void secureDatabase() { getJdbcTemplate().query("select username, password from users", new RowCallbackHandler(){ @Override public void processRow(ResultSet rs) throws SQLException { String username = rs.getString(1); String password = rs.getString(2); String encodedPassword = passwordEncoder.encodePassword(password, null); getJdbcTemplate().update("update users set password = ? where username = ?", encodedPassword,username); logger.debug("Updating password for username: "+username+" to: "+encodedPassword); } }); } }
Код использует Spring JdbcTemplate функциональность для прохода через всех пользователей в базе данных и шифруя пароль, используя PasswordEncoder ссылку. Каждый пароль обновляется индивидуально.
Нам нужно настроить объявление Spring бина, так, чтобы бин мог иницилизироваться при запуске web приложения и после <embedded-database> бина. Spring зависимость бина гарантирует, что DatabasePasswordSecurerBean запускается в нужное время.
<bean class="com.packtpub.springsecurity.security. DatabasePasswordSecurerBean" init-method="secureDatabase" depends-on="dataSource"> <property name="dataSource" ref="dataSource"/> </bean>
Если вы сейчас запустите web приложение, вы увидите, что пароль в базе данных зашифрован, и вход в систему работает нормально.
Если заказчик начнет проверять зашифрованные пароли в базе данных, он снова найдет то, что вызовет у него беспокойство по-поводу безопасности сайта. Давайте посмотрим, как храняться значения имени пользователя и пароля для нашего admin и guest пользователей:
Имя пользователя | Обычный текст пароля | Зашифрованный пароль |
admin | admin | 7b2e9f54cdff413fcde01f330af6896c3cd7e6cd |
guest | guest | 2ac15cab107096305d0274cd4eb86c74bb35a4b4 |
Это выглядит очень защищенным - зашифрованные пароли, очевидно, не имеют никакого сходства с оригинальными паролями. Что же может обеспокоить заказчика? Что если мы добавим нового пользователя у которого такой же пароль как и у admin пользователя.
Имя пользователя | Обычный текст пароля | Зашифрованный пароль |
fakeadmin | admin | 7b2e9f54cdff413fcde01f330af6896c3cd7e6cd |
Теперь обратите внимание, зашифрованный пароль пользователя fakeadmin тот же самый, что и у настоящего admin пользователя. Таким образом, хакер может получить доступ к web приложению.
Один общий и эффективный метод добавления другого слоя безопасности к шифрованию пароля - это включение salt. Salt - это вторая часть текста, которая объединяется с текстом пароля до шифрования, с тем чтобы гарантировать, что два фактора должны быть использованы для генерирования (и сравнения) значений шифрованного пароля. Правильно подобранный salt может гарантировать, что не будет двух одинаковых значений зашифрованных паролей.
Salt, как правило, делится на две категории:
Например, следующая диаграмма показывает простой случай, где salt такой же как и имя пользователя:
Spring Security предоставляет для нас интерфейс, o.s.s.authentication.dao.SaltSource, который определяет метод для возвращения значения salt из UserDetails объекта, вместе с двумя реализациями "из коробки":
Как и с конфигурацией базового шифрования пароля, которую мы обсуждали ранее, добавим элементы для поддержки salt паролей в bootstrap код и в DaoAuthenticationProvider. Мы можем изучить то, как процесс salt пароля изменяет bootstrap и аутентификацию путем пересмотра диаграммы встречавшейся нам ранее:
В dogstore-base.xml добавим объявление бина для SaltSource:
<bean class="org.springframework.security.authentication. dao.ReflectionSaltSource" id="saltSource"> <property name="userPropertyToUse" value="username"/> </bean>
Мы нстроили salt source для использования свойства username, но это сделано не надолго, только для целей демонстрации. Позже мы это изменим.
Нам потребуется подключить SaltSource к PasswordEncoder, таким образом, чтобы учетные данные пользователя, представленные при входе, могли надлежащим образом salted, до сравнения с хранимыми значениями. Это достагается путем добавления нового объявления в dogstore-security.xml:
<authentication-manager alias="authenticationManager"> <authentication-provider user-service-ref="jdbcUserService"> <password-encoder ref="passwordEncoder"> <salt-source ref="saltSource"/> </password-encoder> </authentication-provider> </authentication-manager>
Заметьте, что, если вы запустите приложение прямо сейчас, то не сможете войти в систему. Также как и в предыдущем упражнении, bootstrap бин шифрования пароля базы данных должен быть изменен для включения SaltSource.
Мы добавим бин ссылку на DatabasePasswordSecurerBean, а также ссылку на UserDetailsService, чтобы мы могли получить подходящий salt пароль для пользователя:
public class DatabasePasswordSecurerBean extends JdbcDaoSupport { @Autowired private PasswordEncoder passwordEncoder; @Autowired private SaltSource saltSource; @Autowired private UserDetailsService userDetailsService; public void secureDatabase() { getJdbcTemplate().query("select username, password from users", new RowCallbackHandler(){ @Override public void processRow(ResultSet rs) throws SQLException { String username = rs.getString(1); String password = rs.getString(2); UserDetails user = userDetailsService.loadUserByUsername(username); String encodedPassword = passwordEncoder.encodePassword(password, saltSource.getSalt(user)); getJdbcTemplate().update("update users set password = ? where username = ?", encodedPassword, username); logger.debug("Updating password for username: "+username+" to: "+encodedPassword); } }); } }
Напомним, что SaltSource опирается на UserDetails объект для генерирования salt значения. На данный момент, мы не имеем соответствия строк базы данных к UserDetails объекту, мы должны сделать запрос к UserDetailsService (наш CustomJdbcDaoImpl), чтобы посомтреть UserDetails на основе имени пользователя из SQL ответа (ПРОВЕРИТЬ!!!).
На данный момент, мы должны запустить наше приложение и удачно зайти в систему. Если вы попробуете добавить нового пользователя с тем же паролем к bootstrap SQL скрипту, вы заметите, что пароль генерируемый для пользователя - разный, потому что мы salting пароль с именем пользователя. Это делает пароль более защищенным. Тем не менее эта техника с использованием имени пользователя не совсем хорошая. Почему? На этот вопрос мы ответим чуть позже.
Одно важное изменение которое мы должны сделать, это обновить changePassword функциональность, чтобы сослаться на реализацию кодировщика пораля. Это простое добавление ссылки на бин в CustomJdbcDaoImpl классе и несущественное изменение кода в методе changePassword:
public class CustomJdbcDaoImpl extends JdbcDaoImpl { @Autowired private PasswordEncoder passwordEncoder; @Autowired private SaltSource saltSource; public void changePassword(String username, String password) { UserDetails user = loadUserByUsername(username); String encodedPassword = passwordEncoder.encodePassword (password, saltSource.getSalt(user)); getJdbcTemplate().update( "UPDATE USERS SET PASSWORD = ? WHERE USERNAME = ?", encodedPassword, username); }
Использование здесь PasswordEncoder и SaltSource гарантирует, что пароль пользователя правильно salted когда измениться. Любопытно, что PasswordEncoder и SaltSource не поддерживаются JdbcUserDetailsManager, поэтому, если вы используете реализацию JdbcUserDetailsManager как основу для изменений, вы должны заменить небольшой кусочек кода.
Ранее, когда мы впервые настраивали salting пароля, username является достижимым, но не крайне желательным выбором для salt пароля. Причина этого заключается в том, что username находится под контролем пользователя. Если пользователю дать возможность менять свое имя, то злоумышленники могут воспользоваться этим, чтобы вычислить как это работает.
Повышенные меры безопасности по-прежнему будут содержать свойство в UserDetails, но оно всегда будет невидимым и его нельзя будет менять пользователю. Мы добавим свойство к UserDetails объекту, которое будет заполняться случайным образом при создании пользователя. Это свойство станет для пользователя в качестве salt.
Мы должны быть уверены в том, что salt записывается в базе данных вместе с записями пользователя, и так, мы добавим колонку в Spring Security schema базы данных, определенном в security-schema.sql:
create table users( username varchar_ignorecase(50) not null primary key, password varchar_ignorecase(50) not null, enabled boolean not null, salt varchar_ignorecase(25) not null );
Далее, добавим bootstrap salt значения в test-users-groups-data.sql скрипт:
insert into users(username, password,enabled, salt) values ('admin','admin',true,CAST(RAND()*1000000000 AS varchar)); insert into users(username, password,enabled, salt) values ('guest','guest',true,CAST(RAND()*1000000000 AS varchar));
Помните, что мы изменили insert и выбрали salt значения основанные на случайных числах.
Сейчас мы с вами изменим запрос, используемый для получения пользователя из базы данных для того, чтобы добавить новую колонку 'salt'. Мы будем редактировать бин CustomJdbcDaoImpl в dogstore-security.xml:
<beans:bean id="jdbcUserService" class="com.packtpub.springsecurity.security.CustomJdbcDaoImpl"> <beans:property name="dataSource" ref="dataSource"/> <beans:property name="enableGroups" value="true"/> <beans:property name="enableAuthorities" value="false"/> <beans:property name="usersByUsernameQuery"> <beans:value>select username,password,enabled, salt from users where username = ? </beans:value> </beans:property> </beans:bean>
Нам нужна UserDetails реализация, которая отслеживает значение salt хранящееся в базе данных вместе с записью пользователя в базе данных. Мы просто перекроем стандартный Spring User класс. Не забудьте добавить getter и setter для salt, чтобы ReflectionSaltSource password salter мог найти свойство.
package com.packtpub.springsecurity.security; // imports public class SaltedUser extends User { private String salt; public SaltedUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, List<GrantedAuthority> authorities, String salt) { super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); this.salt = salt; } public String getSalt() { return salt; } public void setSalt(String salt) { this.salt = salt; } }
Мы должны переопределить методы JdbcDaoImpl, ответственного за создание экземпляра UserDetails реализации, которая устанавливает значения по умолчанию для User. Это происходит во время загрузка User из базы данных, и затем копирует User в экземпляр, который берется из UserDetailsService:
public class CustomJdbcDaoImpl extends JdbcDaoImpl { public void changePassword(String username, String password) { getJdbcTemplate().update( "UPDATE USERS SET PASSWORD = ? WHERE USERNAME = ?" password, username); } @Override protected UserDetails createUserDetails(String username, UserDetails userFromUserQuery, List<GrantedAuthority> combinedAuthorities) { String returnUsername = userFromUserQuery.getUsername(); if (!isUsernameBasedPrimaryKey()) { returnUsername = username; } return new SaltedUser(returnUsername, userFromUserQuery.getPassword(),userFromUserQuery.isEnabled(), true, true, true, combinedAuthorities, ((SaltedUser) userFromUserQuery).getSalt()); } @Override protected List<UserDetails> loadUsersByUsername(String username) { return getJdbcTemplate(). query(getUsersByUsernameQuery(), new String[] {username}, new RowMapper<UserDetails>() { public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException { String username = rs.getString(1); String password = rs.getString(2); boolean enabled = rs.getBoolean(3); String salt = rs.getString(4); return new SaltedUser(username, password, enabled, true, true, true, AuthorityUtils.NO_AUTHORITIES, salt); } }); } }
Методы createUserDetails и loadUsersByUsername адаптированы из методов суперкласса - изменения методов суперкласса подсвечены.
Поздравляем, наш заказчик рад, что наши защищенные пароли хранятся в базе данных - хорошая работа.
Перемещаем remember me в базу данных
На данный момент, все работает очень хорошо пока сервер не стал перегружаться, если это случилось, то пользовательская сессия будет утеряна. Это может быть не совсем удобно для наших пользователей.
К счастью, Spring Security предоставляет возможность для сохранения rememberme tokens в любой реализации o.s.s.web.authentication.rememberme.PersistentTokenRepository интерфейса.
Сейчас мы будем изменять конфигурацию remember me для поддержки базы данных. Spring Security парсер распознает новый атрибут data-source-ref в <remember-me> объевлении и просто переключает реализации классов для RememberMeServices. Давайте рассмотрим шаги необходимые для осуществления этого.
Мы разместим SQL файл, содержащий объявление schema в classpath в WEB-INF/classes, рядом с другим bootstrap SQL скриптом, с которым мы уже работали. Давайте назовем этот скрипт remember-me-schema.sql:
create table persistent_logins ( username varchar_ignorecase(50) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);
Добавление нового SQL скрипта в объявление базы данных
Далее, добавим ссылку на наш новый SQL скрипт, который мы только что создали в <embedded-database> в dogstore-security.xml:
<jdbc:embedded-database id="dataSource" type="HSQL"> <jdbc:script location="classpath:security-schema.sql"/> <jdbc:script location="classpath:remember-me-schema.sql"/> <jdbc:script location="classpath:test-users-groups-data.sql"/> </jdbc:embedded-database>
Настройка remember me сервиса для сохранения в базу данных
В конце, мы должны сделать небольшую настройку в <remember-me> объявлении:
<http auto-config="true" use-expressions="true" access-decision-manager-ref="affirmativeBased"> <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')"/> <form-login login-page="/login.do" /> <remember-me key="jbcpPetStore" token-validity-seconds="3600" data-source-ref="dataSource"/> <logout invalidate-session="true" logout-success-url="" logout-url="/logout"/> </http>
Это все, что нужно. Сейчас, если вы перезапустите приложение, оно больше не будет забывать пользователей, которые недавно имели действительный remembe me куки.
Copyright © 2024 |