SPRING-SOURCE.RU
Исходный код:
Настраиваем базу данных
(Начинаем работать с базами данных)
Смена пароля и БД
(Смена пароля с использованием базы данных)
Детальная настройка JdbcDaoImpl
(Другой подход работы с базой данных. Этот пример не рабочий, он лишь отражает пример описанный в третьей части. Чтобы все работало вы должны подставить свои SQL данные)
Последнее обновление 18.08.2010

Защита с использованием хранилища учетных данных

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

В этой части мы пойдем в сторону использования базы данных в качестве хранилища для аутентификации. Мы будем изучать Spring Security schema базы данных, и будем искать пути чтобы расширить JDBC реализацию.

В ходе изучения этой части мы познакомимся:

База данных и аутентификация с Spring Security

Очевидным вопросом на счет нашей безопасности является то, что хранение в памяти пользователей и паролей имеет очень непродолжительное время жизни. Как только приложение перегружается, любые новые регистрации пользователей или любые другие действия будут потеряны. Это неприемлемо, поэтому следующим логическим шагом будет переконфигурирование Spring Security для использования базы данных в качестве хранилища аутентификационных данных. Использование JDBC реляционных баз данных позволит пользовательским данным сохраняться даже, если сервер приложения перегрузился. Это является более типичным отражением Spring Security в реальном мире.

Настройка базы данных для хранения аутентфикации

Первая часть этого упражнения включает создание, основанной на Java, реляционной базы данных HyperSQL DB (HSQL). Мы будем настраивать HSQL для запуска в режиме in-memory.

Создание Spring Security schema по умолчанию

Мы предоставляем SQL файл, security-schema.sql, который будет создавать все таблицы необходимые для реализации Spring Security, используя HSQL. Если вы используете свою базу данных, то подкорректируйте этот файл. Мы кладем этот файл в WEB-INF/classes.

Настройка HSQL

Для настройки 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> создает базу данных только в памяти.

Настройка JdbcDaoImpl хранилища данных аутентификации

Мы редактировали 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>
		

Добавление пользовательских определений в schema

В конце, мы создаем другой 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 получить и подтвердить информацию о принципале из хранилища учетных данных. Мы могли увидеть это из диаграммы во второй части:

Spring Security

Отличия между нашей конфигурацией базы данных как хранилища аутентификационных данных и хранения в памяти, составляет реализация UserDetailsService. Класс o.s.s.core.userdetails.jdbc.JdbcDaoImpl предоставляет реализацию UserDetailsService. Вместо того, чтобы обращаться в хранилище в памяти, JdbcDaoImpl смотрит пользователя в базе данных.

Spring Security

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


Реализация пользовательского JDBC UserDetailsService

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

Создаем пользовательский JDBC UserDetailsService класс

Класс будет находиться в пакете 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 для пользовательского UserDetailsService

Добавим следующее объявление Spring Bean в dogstore-base.xml в конфигурационном файле Spring:

                        
<bean id="jdbcUserService"
    class="com.packtpub.springsecurity.security.CustomJdbcDaoImpl">
  <property name="dataSource" ref="dataSource"/>
</bean>
		

Управление пользователем JDBC "из коробки"

Мы с вами расширяли 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

JdbcDaoImpl имеет несколько опций, которые позволяют адаптировать его к использованию в существующей schema, или делать более сложные изменения в существующих возможностях. Во многих случаях можно адаптировать JDBC UserDetailsService прямо, что называется, "из коробки", без использования дополнительного кода.

Одна важная особенность - способность добавлять уровни скрытности (косвенности - indirection) между пользователями и объявлениями GrantedAuthority, группируя GrantedAuthority в логические наборы, называемыми groups - группы.

Spring Security

Как видно на диаграмме, эта косвенность (indirection) позволяет назначать тот же набор ролей к нескольким пользователям, просто добавляя нового пользователя в существующую группу.

Это объединение общих наборов полномочий может быть полезно в следующих случаях:

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

Настройка авторизации основанной на группах

Мы добавили две группы к сайту - обычные пользователи, которых мы называем "Users", и администраторы, которых мы называем "Administrators". Существующие guest и admin учетные записи пользователей будут брошены в подходящие группы через изменения в нашем SQL скрипте, который мы использовали для создания базы данных.

Настроим JdbcDaoImpl для использования групп

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

Для простоты мы создадим новый 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>
		

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

Давайте, в данный момент, посмотрим на важный кусочек конфигурации.

Использование существующей или пользовательской schema с базой данных, использующейся для аутентификации

Как правило, новые пользователи Spring Security начинают свой опыт через адаптирование JDBC пользователя, группы или роли к существующей schema. Хотя существующая база данных не соответствует существующей schema Spring Security. Мы можем настроить JdbcDaoImpl для этого соотношения.

Представьте, что существующая schema базы данных похожа на следующую диграмму:

Spring Security

Мы можем просто изменить конфигурацию JdbcDaoImpl для использования этой schema и заменить ожидаемые определения таблиц Spring Security и колонок, которые мы используем для нашего сайта.

Определение правильных JDBC SQL запросов

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, но они должны быть возвращены в любом случае. Прежде чем двигаться дальше давайте попробуем написать эти запросы для диаграммы к базе данных, которую мы нарисовали ранее.

Настройка JdbcDaoImpl для использования пользовательских SQL запросов

В случае использования пользовательских 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

Шифрование пароля в 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

Первым делом мы объявим экземпляр PasswordEncoder как Spring бин:

                        
<bean class="org.springframework.security.authentication.
  encoding.ShaPasswordEncoder" id="passwordEncoder"/>
		

Вы могли заметить, что мы использовали SHA-1 PasswordEncoder реализацию. Это эффективный алгоритм шифрования, часто используемый для хранения паролей.

Настройка AuthenticationProvider

Нам нужно настроить 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 кодом.

Пишем bootstrap шифрование паролей базы данных

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

Настройка bootstrap шифрования пароля

Нам нужно настроить объявление 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 приложение, вы увидите, что пароль в базе данных зашифрован, и вход в систему работает нормально.

Хотите немного salt с этим паролем?

Если заказчик начнет проверять зашифрованные пароли в базе данных, он снова найдет то, что вызовет у него беспокойство по-поводу безопасности сайта. Давайте посмотрим, как храняться значения имени пользователя и пароля для нашего admin и guest пользователей:

Имя пользователя Обычный текст пароля Зашифрованный пароль
admin admin 7b2e9f54cdff413fcde01f330af6896c3cd7e6cd
guest guest 2ac15cab107096305d0274cd4eb86c74bb35a4b4

Это выглядит очень защищенным - зашифрованные пароли, очевидно, не имеют никакого сходства с оригинальными паролями. Что же может обеспокоить заказчика? Что если мы добавим нового пользователя у которого такой же пароль как и у admin пользователя.

Имя пользователя Обычный текст пароля Зашифрованный пароль
fakeadmin admin 7b2e9f54cdff413fcde01f330af6896c3cd7e6cd

Теперь обратите внимание, зашифрованный пароль пользователя fakeadmin тот же самый, что и у настоящего admin пользователя. Таким образом, хакер может получить доступ к web приложению.

Один общий и эффективный метод добавления другого слоя безопасности к шифрованию пароля - это включение salt. Salt - это вторая часть текста, которая объединяется с текстом пароля до шифрования, с тем чтобы гарантировать, что два фактора должны быть использованы для генерирования (и сравнения) значений шифрованного пароля. Правильно подобранный salt может гарантировать, что не будет двух одинаковых значений зашифрованных паролей.

Salt, как правило, делится на две категории:

Например, следующая диаграмма показывает простой случай, где salt такой же как и имя пользователя:

Spring Security

Spring Security предоставляет для нас интерфейс, o.s.s.authentication.dao.SaltSource, который определяет метод для возвращения значения salt из UserDetails объекта, вместе с двумя реализациями "из коробки":

Настройка salt пароля

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

Spring Security

Объявление SaltSource Spring бина

В dogstore-base.xml добавим объявление бина для SaltSource:

                        
<bean class="org.springframework.security.authentication.
    dao.ReflectionSaltSource" id="saltSource">
  <property name="userPropertyToUse" value="username"/>
</bean>
		

Мы нстроили salt source для использования свойства username, но это сделано не надолго, только для целей демонстрации. Позже мы это изменим.

Связывание PasswordEncoder с SaltSource

Нам потребуется подключить 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

Мы добавим бин ссылку на 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 как основу для изменений, вы должны заменить небольшой кусочек кода.

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

Ранее, когда мы впервые настраивали salting пароля, username является достижимым, но не крайне желательным выбором для salt пароля. Причина этого заключается в том, что username находится под контролем пользователя. Если пользователю дать возможность менять свое имя, то злоумышленники могут воспользоваться этим, чтобы вычислить как это работает.

Повышенные меры безопасности по-прежнему будут содержать свойство в UserDetails, но оно всегда будет невидимым и его нельзя будет менять пользователю. Мы добавим свойство к UserDetails объекту, которое будет заполняться случайным образом при создании пользователя. Это свойство станет для пользователя в качестве salt.

Расширение schema базы данных

Мы должны быть уверены в том, что 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 значения основанные на случайных числах.

Тонкая настройка конфигурации пользовательского CustomJdbcDaoImpl UserDetails сервиса

Сейчас мы с вами изменим запрос, используемый для получения пользователя из базы данных для того, чтобы добавить новую колонку '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 реализации

Нам нужна 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;
  }
}
		

Расширение функциональности CustomJdbcDaoImpl

Мы должны переопределить методы 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 tokens

Сейчас мы будем изменять конфигурацию remember me для поддержки базы данных. Spring Security парсер распознает новый атрибут data-source-ref в <remember-me> объевлении и просто переключает реализации классов для RememberMeServices. Давайте рассмотрим шаги необходимые для осуществления этого.

Добавление SQL для создания remember me schema

Мы разместим 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 куки.