Spring Security/Ключевые сервисы Spring Security
Эта статья представляет собой перевод Spring Security Reference Documentation, Ben Alex, Luke Taylor 3.0.2.RELEASE, Глава 6, Ключевые сервисы.
Теперь, после высокоуровневого обзора архитектуры Spring Security и ее основных классов, давайте познакомимся поближе с парой-тройкой основных интерфейсов и их реализацией, в особенности с AuthenticationManager
, UserDetailsService
и AccessDecisionManager
. Они регулярно упоминаются в оставшейся части этого документа, поэтому важно знать, как они настраиваются и работают.
AuthenticationManager, ProviderManager и AuthenticationProvider'ы
правитьAuthenticationManager
это просто интерфейс, поэтому реализация может быть какой угодно, в зависимости от нашего выбора, но как это работает на практике? Что делать, если нам нужно использовать несколько баз данных с аутентификационной информацией, или сочетание различных сервисов аутентификации, таких как база данных и сервер LDAP?
Реализация по умолчанию в Spring Security называется ProviderManager
и вместо того, чтобы самостоятельно обрабатывать аутентификационный запрос, он делегирует это списку настроенных AuthenticationProvider
'ов, каждый из которых в свою очередь запрашивается, может ли он выполнить аутентификацию. Каждый провайдер либо сгенерирует исключение, либо вернет полностью заполненный объект Authentication
. Помните наших хороших друзей, UserDetails
и UserDetailsService
? Если нет, то вернитесь к предыдущей главе и освежите память. Наиболее распространенный подход к проверке аутентификационного запроса это, загрузить соответствующий UserDetails
и сверить загруженные пароль с тем, что был введен пользователем. Это подход используется DaoAuthenticationProvider
(см. ниже). Загруженный UserDetails
объект - и в частности GrantedAuthority
в нем - будут использоваться при создании полностью заполненного объекта Authentication
, который возвращается в случае успешной аутентификации и сохраняется в SecurityContext
.
Если вы используете конфигурирование с помощью пространства имен, то экземпляр ProviderManager
создается и поддерживается автоматически, и вы только добавляете провайдеров аутентификации с помощью элементов пространства имен(см. Главу «Пространство имен»). В этом случае, вы не должны объявлять bean для ProviderManager
в контексте приложения. Однако, если вы не используете пространство имен, то вы должны объявить его следующим образом:
<bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
<property name="providers">
<list>
<ref local="daoAuthenticationProvider"/>
<ref local="anonymousAuthenticationProvider"/>
<ref local="ldapAuthenticationProvider"/>
</list>
</property>
</bean>
В приведенном выше примере имеется три провайдера. Они будут использоваться при попытке проведения аутентификации в том порядке, в каком они указаны в списке (что подразумевается при использовании List), причем каждый провайдер может попытаться либо выполнить аутентификацию, либо вернет null
. Если все реализации вернут null
, то ProviderManager
поднимет ProviderNotFoundException
. Если вам интересно получить дополнительную информацию о цепочке провайдеров, то обратитесь к описанию ProviderManager
в Javadocs.
Ссылка на ProviderManager
встраивается в механизмы аутентификации, такие как фильтр обработки веб-формы аутентификации и он будет вызываться для обработки аутентификационных запросов. Провайдеры которые вам потребуется иногда будут взаимозаменяемы для механизмов аутентификации, а иногда будут зависеть от конкретного механизма аутентификации. Например, DaoAuthenticationProvider
и LdapAuthenticationProvider
совместимы с любым механизмом, который отправляет в запросе имя пользователя/пароль, и следовательно аутентификационный запрос будет работать как с использованием веб-формы или базовой HTTP аутентификации. С другой стороны, некоторые механизмы аутентификации создают такие объекты запроса аутентификации, которые могут быть понятны только одному типу AuthenticationProvider
. Примером может служить JA-SIG CAS, который использует понятие сервисного тикета и таким образом может пройти аутентификацию только с помощью CasAuthenticationProvider
. Вы не должны слишком беспокоиться по этому поводу, потому что если вы забудете зарегистрировать подходящий провайдер, то просто получите ProviderNotFoundException
при попытке выполнить аутентификацию.
DaoAuthenticationProvider
правитьПростейшим AuthenticationProvider
'ом реализованным в Spring Security является DaoAuthenticationProvider
, который также является одним из первых поддерживаемых провайдеров в этом каркасе. Он опирается на UserDetailsService
(как DAO) для поиска имя пользователя, пароля и GrantedAuthority
. Он идентифицирует пользователей просто сравнивая пароль присланный в UsernamePasswordAuthenticationToken
с паролем, который был загружен UserDetailsService
. Настройка провайдера довольно проста:
<bean id="daoAuthenticationProvider"
class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="inMemoryDaoImpl"/>
<property name="saltSource" ref bean="saltSource"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
</bean>
PasswordEncoder
и SaltSource
являются необязательными. PasswordEncoder
обеспечивает кодирование и декодирование паролей, представленных в объекте UserDetails
, который возвращается настроенным UserDetailsService
. SaltSource
позволяет "добавлять соль" в пароли, что повышает безопасность паролей в аутентификационном репозитории. Ниже это будет обсуждаться более подробно.
Реализация UserDetailsService
правитьКак отмечалось ранее, большинство провайдеров аутентификации пользуются интерфейсами UserDetails
и UserDetailsService
. Напомним, что контракт для UserDetailsService
один единственный метод:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
Возвращаемый UserDetails
представляет собой интерфейс, который предоставляет геттеры, которые гарантируют not-null
результаты аутентификационной информации, такие как имя пользователя, пароль, предоставленные полномочия и является ли учетная запись пользователя заблокированной или нет. Большинство провайдеров аутентификации будет использовать UserDetailsService
, даже если имя пользователя и пароль по факту не используются для принятия решения об аутентификации. Они могут использовать объект UserDetails
просто для получения информации о GrantedAuthority
, потому что некоторые другие системы (например, LDAP и X.509 или CAS и т.д.) берут на себя ответственность за фактическое подтверждение полномочий.
Данный UserDetailsService
настолько прост в реализации, что пользователи смогут легко запросить аутентификационную информацию с использованием одной из стратегий сохранения данных. Надо сказать, что Spring Security включает пару полезных базовых реализаций этого интерфейса, которые мы рассмотрим ниже.
Аутентификация In-Memory
правитьМожно легко создать реализацию UserDetailsService
, которая будет извлекать информацию из какого-то хранилища, но множество приложений не требуют таких сложностей. Это особенно верно, если вы создаете прототип приложения или просто начинаете интеграцию Spring Security в свое приложение, когда вы не хотите тратить время на настройку базы данных или написание реализации UserDetailsService
. В такой ситуации, самый простой вариант, использовать элемент user-service из пространства имен security:
<user-service id="userDetailsService">
<user name="jimi" password="jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="bob" password="bobspassword" authorities="ROLE_USER" />
</user-service>
Также поддерживается использование внешнего файла с необходимыми свойствами:
<user-service id="userDetailsService" properties="users.properties"/>
Свойства в файле должны быть заданы в следующей форме:
username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]
Например:
jimi=jimispassword,ROLE_USER,ROLE_ADMIN,enabled
bob=bobspassword,ROLE_USER,enabled
JdbcDaoImpl
правитьSpring Security также включает реализацию UserDetailsService
, которая может получать аутентификационную информацию из источника данных JDBC. Внутри Spring, JDBC используется, чтобы избежать сложности использования полнофункционального объектно-реляционного маппера (ORM), только для того чтобы хранить информацию о пользователе. Если приложение уже использует один из ORM инструментов, то можно предпочесть написание собственного UserDetailsService
для повторного использования уже готовой объектно-реляционной структуры, которую вы, вероятно, уже создали. Возвращаясь к JdbcDaoImpl
, ниже приведен пример конфигурации:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
<bean id="userDetailsService" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
Вы можете использовать различные СУБД, просто изменяя DriverManagerDataSource
, как это показано выше. Вы можете так же использовать глобальные источники данных получаемые из JNDI или из любых конфигураций Spring'а.
Авторизация групп пользователей
правитьПо умолчанию, JdbcDaoImpl
загружает полномочия для одного пользователя, предполагая, что полномочия назначаются непосредственно пользователям (см. схему базы данных в приложении). Альтернативный подход заключается в разделении полномочий по группам и назначение групп пользователям. Некоторые предпочитают этот подход для управления правами пользователей. См. JdbcDaoImpl
Javadoc для дополнительной информации о том, как включить использование групповых полномочий. Схема базы данных для использования групп также включена в приложение.
Шифрование пароля
правитьИнтерфейс PasswordEncoder
используется для поддержки работы с паролями, которые хранятся в зашифрованном виде. Обычно это означает, что пароли "хэшируется" с использованием алгоритма построения дайджеста, такого как например MD5 или SHA.
Что такое «хеш»?
правитьХэширования паролей не уникальное свойство Spring Security, но является распространенным источником путаницы для пользователей, которые не знакомы с данной концепцией. Хэш- (или дайджест) алгоритм представляет собой одностороннюю функцию, которая производит некоторые выходные данные фиксированной длинны (хеш) из входных данных произвольной длинны, таких например, как пароль. MD5 хэш строки "password" (в шестнадцатеричной системе) выглядит следующим образом:
5f4dcc3b5aa765d61d8327deb882cf99
Хеш является "односторонним" в том смысле, что очень трудно (практически невозможно) получить оригинальные входные данные на основе которых было получено хеш-значение, либо вообще какие-либо данные, которые приведут к получению такого же хеш-значения. Это свойство делает хеш-значения очень полезными для аутентификации. Они могут быть сохранены в базе данных вместо нешифрованных паролей и даже если возникнет угроза похищения этих данных, то пароль используемый для аутентификации не будет сразу же раскрыт. Заметим, это также означает, что не будет возможности восстановления пароля, когда он будет храниться в закодированном виде.
Добавление «Соли» к Хешу
правитьСуществует одна потенциальная проблема с использованием хеш-значения для паролей, можно сравнительно легко обойти свойство однонаправленности хеша, если в качестве входных данных используются частоупотребляемые слова. Например, если поищете в Google хэш-значение 5f4dcc3b5aa765d61d8327deb882cf99 помощью Google, то быстро найдете оригинальные слово "password". Аналогичным образом, злоумышленник может построить словарь хэшей из стандартного списка слов и использовать его для поиска оригинального пароля. Один из способов избежать этого является наличие соответствующей строгой политики создания паролей, чтобы попытаться предотвратить использование часто встречающихся слов. Также можно использовать "соль" при вычислении хэшей. Это дополнительная строка каких то данных для каждого пользователя, которые комбинируются с паролем до вычисления хэша. В идеале данные должны быть случайной последовательностью, но на практике лучше любое значение для соли чем его отсутствие. В Spring Security имеется интерфейс SaltSource
, который может быть использован аутентификационным провайдером для генерирования значения соли для конкретного пользователя. Использование соли означает, что злоумышленник будет вынужден построить отдельный словарь хэшей для каждого значения соли, что делает атаку более затруднителной (но не невозможной).
Хеширование и аутентификация
правитьКогда аутентификационному провайдеру (такому как DaoAuthenticationProvider
) нужно сверить пароль, присланный в аутентификационном запросе со значением, которое известно для данного пользователя и пароль хранится в закодированном виде, то тогда присланное значение должно быть закодировано с помощью точно такого же алгоритма. Совместимость алгоритмов это полностью ваша зона ответственности. Spring Security не может контролировать хранимые значения. Если вы добавили хеширование паролей в конфигурацию аутентификации Spring Security, а в базе данных пароли хранятся в виде обычного не шифрованного текста, то в это случае аутентификация не произойдет ни при каких обстоятельствах. Например, даже если вы знаете что в базе данных используется MD5 кодирование и ваше приложение сконфигурировано так, чтобы использовать Md5PasswordEncoder
из состава Spring Security, то все равно есть возможность получить ошибку. В базе данных кодированные пароли могут храниться в виде Base64 , а кодировщик Spring Security например будет использовать строки с шестнадцатеричными значениями (по умолчанию) [5]. Или в базе данных сведения будут храниться в верхнем регистре, а результат работы кодировщика будет в нижнем регистре. Обязательно напишите тест, который будет сверять результат работы настроенного кодировщика (на основе известного пароля и «соли») с данными хранящимися в базе данных, до того как двигаться дальше и применять механизм аутентификации в вашем приложении. Для получения дополнительной информации, как по умолчанию происходит слияния соли и пароля, см. Javadoc для BasePasswordEncoder
. Если вы захотите создавать закодированные пароли непосредственно в Java коде и уже их сохранять в базе данных, то вы можете использовать метод encodePassword
класса PasswordEncoder
.