Процесс предоставления пользователю доступа к информационной системе состоит из трёх этапов: идентификации, аутентификации и авторизации.
Идентификация — поиск пользователя по некоторому идентификатору, который он передаёт системе. В качестве идентификатора может выступать логин, адрес электронной почты, номер телефона, в общем, любая информация, позволяющая однозначно идентифицировать пользователя в рамках системы.
После того как пользователь идентифицирован, его требуется аутентифицировать или подтвердить его подлинность. Для подтверждения своей подлинности пользователь должен передать системе некоторую секретную информацию, которую в идеальных условиях должен знать только он, а система в свою очередь должна иметь возможность проверить корректность секретной информации. В качестве секретной информации могут выступать:
- Обычный пароль
- Одноразовый пароль, сгенерированный по алгоритму HOTP или TOTP
- Код, присланный в CMC
- Последние цифры номера телефона, с которого поступит звонок
В общем, секретной информацией может выступать любая информация, которую на момент аутентификации должен знать только пользователь. Аутентификация при этом может быть многофакторной для повышения уровня безопасности и снижения вероятности получения несанкционированного доступа. Для этого у пользователя может требоваться несколько видов секретной информации при аутентификации, например, пароль и одноразовый пароль, сгенерированный по алгоритму TOTP в приложении Google Authenticator или аналогичном.
Со стороны пользователя идентификация и аутентификация являются одним процессом, так как данные для выполнения обоих действий — логин и пароль, пользователь отправляет одновременно, однако на стороне сервиса это два последовательных действия.
После того как пользователь идентифицирован и аутентифицирован, наступает этап авторизации — проверки наличия прав у пользователя к запрашиваемому ресурсу. Про авторизацию в целом я буду рассказывать в отдельном материале, а здесь лишь упомяну, что глобально существует два подхода к реализации авторизации: на основе аттрибутов пользователя (Attribute Based Access Control; ABAC), например, ролей (Role Based Access Control; RBAC), и на основе списка правил доступа (Access Control List; ACL). ABAC является наиболее распространённым способом реализации авторизации в веб-приложениях, в основном благодаря простоте реализации, в то время как ACL применяется в более сложных проектах, как правило, корпоративного и государственного сегментов, так как позволяет более гибко настраивать политики доступа.
Реализация аутентификации в Spring Security
Прежде чем переходить к конкретным способам идентификации и аутентификации пользователя в Spring Security, хотелось бы в общих чертах разобрать этот процесс и обозначить основные компоненты, реализующие его.
Фильтр аутентификации
Как было сказано в предыдущей статье, основными компонентами Spring Security являются фильтры, и аутентификация не является исключением — отправной точкой процесса идентификации и аутентификации является соответствующий фильтр аутентификации. Так Basic-аутентификация инициируется фильтром BasicAuthenticationFilter. Для упрощения процесса написания фильтров для обработки запросов Spring Security предоставляет абстрактные реализации, среди которых есть OncePerRequestFilter
, ориентированный на работу с HTTP-запросами.
Первым делом фильтр должен определить, что пользователь пытается аутентифицироваться. Сделать это он может по наличию данных, специфичных для конкретного способа аутентификации, например, какого-то заголовка или параметров запроса. Из полученных данных фильтр должен сформировать запрос аутентификации — создать экземпляр класса, реализующего интерфейс Authentication
.
Созданный запрос аутентификации для дальнейшей обработки фильтр должен передать менеджеру аутентификации — AuthenticationManager
. В целом при создании собственных фильтров аутентификации вы можете реализовывать весь процесс аутентификации в фильтре, не используя остальные компоненты, но в этом случае вы отказываетесь от гибкости архитектуры Spring Security. В случае успешной аутентификации AuthenticationManager
вернёт результат аутентификации, который тоже является экземпляром класса, реализующего интерфейс Authentication
, но запрос и результат аутентификации — разные объекты.
Полученный результат аутентификации фильтр должен сохранить в новом контексте безопасности — SecurityContext
, а после этого продолжить исполнение цепочки фильтров безопасности, вызвав метод FilterChain.filter(request, response)
. Важно помнить, что менеджер аутентификации может вернуть только успешный результат аутентификации, а если по какой-то причине аутентифицировать пользователя не получилось — выбросить соответствующее исключение.
Если в процессе аутентификации произошла ошибка, то фильтр должен очистить контекст безопасности. Кроме этого в большинстве случаев фильтр должен снова запросить у пользователя данные для идентификации или аутентификации при помощи точки входа аутентификации — AuthenticationEntryPoint
, после чего нужно приостановить обработку запроса. Обработка запроса приостанавливается, если отсутствует вызов метода FilterChain.filter(request, response)
.
Если фильтр допускает продолжение обработки запроса даже при ошибке аутентификации, то он должен продолжить исполнение цепочки фильтров безопасности, вызвав метод FilterChain.filter(request, response)
.
Если запрос не содержит данных для инициации процесса идентификации и аутентификации пользователя, то фильтр не должен ничего делать, а только продолжить исполнение цепочки фильтров, вызвав метод FilterChain.filter(request, response)
.
В общих чертах любой фильтр аутентификации выглядит следующим образом:
Схематично процесс аутентификации можно описать следующим образом:
Разрабатывать собственные фильтры аутентификации вам понадобится только в том случае, если вам требуется какой-то собственный, специфичный для вашего проекта способ аутентификации. Например, если вы захотите идентифицировать и аутентифицировать пользователя по логину, паролю и одноразовому паролю. Но и в этом случае вам, скорее всего, не придётся разрабатывать собственный фильтр, так как в большинстве случае вы можете использовать AuthenticationFilter
.
Authentication — запрос и результат аутентификации
Запрос и результат аутентификации в Spring Security описывается одним и тем же интерфейсом — Authentication
, однако запрос и результат — разные объекты, содержащие разные данные:
- Метод
getPrincipal()
у запроса возвращает идентификатор пользователя (логин в случае с аутентификацией по логину и паролю), а у результата может возвращать как идентификатор, так и объект с подробными данными успешно аутентифицированного пользователя, например,UserDetails
. - Метод
isAuthenticated()
возвращаетfalse
у запроса иtrue
— у успешного результата.
Более того, в рамках одной попытки аутентификации запрос и результат могут быть экземплярами разных классов.
Интерфейс Authetication
расширяет стандартный для JDK интерфейс Principal
, добавляя методы, необходимые для работы Spring Security:
Для идентификации и аутентификации пользователей с использованием логина и пароля существует класс UsernamePasswordAuthenticationToken
, расширяющий абстрактный класс AbstractAuthenticationToken
:
Обратите внимание на то, что свойства principal
, credentials
и details
имеют тип Object
, это делает их универсальными, но в то же время подразумевает усложнение работы с ними, так как в процессе нужно будет проверять типы фактических значений этих свойств и приводить значения этих свойств к нужным типам.
Собственный тип аутентификации, как и фильтр, вам потребуется в случае реализации собственного способа аутентификации, специфичного для вашего проекта.
Менеджер аутентификации
Второй крупный компонент Spring Security, участвующий в процессе аутентификации пользователя — менеджер аутентификации, экземпляр класса, реализующего интерфейс AuthenticationManager
.
Задача данного компонента — обработать запрос на аутентификацию и вернуть успешный результат аутентификации, либо выбросить исключение. Впрочем, основная стандартная реализация этого интерфейса — ProviderManager
не реализует процесс аутентификации, а делегирует большую его часть другим компонентам — провайдерам аутентификации, экземплярам классов, реализующих интерфейс AuthenticationProvider
.
Реализовывать собственный менеджер аутентификации вам, скорее всего, не понадобится, так как стандартные реализации универсальны и могут использоваться практически во всех ситуациях.
Провайдер аутентификации
Провайдеры аутентификации описываются интерфейсом AuthenticationProvider
:
В процессе обработки запроса аутентификации провайдер должен получить данные о пользователе из некоторого источника, будь то база данных, сторонний сервис или параметры JWT, провалидировать с их помощью данные запроса и из них же заполнить свойства principal
, credentials
и authorities
.
Каждый провайдер поддерживает какой-то специфичный способ аутентификации, так AbstractUserDetailsAuthenticationProvider
поддерживает только UsernamePasswordAuthenticationToken
. В контексте безопасности приложения может быть сконфигурировано несколько провайдеров для поддержки разных способов аутентификации. Более того, контекст безопасности приложения может содержать несколько провайдеров, способных обработать один и тот же тип запроса аутентификации. Это может быть полезно в случаях, когда провайдеры имеют разную логику обработки запроса аутентификации или источники данных о пользователях. В такой ситуации один провайдер может не аутентифицировать пользователя, а другой — успешно это сделать.
Свой провайдер аутентификации вам может потребоваться в случае, если вы реализуете свой способ аутентификации.
Данные о пользователе
Для создания корректного результата аутентификации необходимы данные о пользователе. В Spring Security для описания пользователя существует интерфейс UserDetails
:
Стоит отметить, что Spring Security не взаимодействует с этим интерфейсом и его реализациями напрямую, следовательно, в качестве источника информации о пользователе может выступать любой класс или интерфейс. Единственное ограничение — класс или интерфейс, описывающий пользовательские данные, должен реализовывать или расширять интерфейс Serializable
, так как пользовательские данные сериализуются при сохранении в HTTP-сессии.
Контекст безопасности
Контекст безопасности описывается интерфейсом SecurityContext
и используется для хранения успешной аутентификации:
Данный интерфейс расширяет интерфейс Serializable
, так как экземпляры классов-наследников могут быть сериализованы при сохранении контекста безопасности в репозитории.
Для доступа к текущему контексту безопасности следует использовать экземпляры классов, реализующих интерфейс SecurityContextHolderStrategy
, как это было показано в примере кода фильтра выше. Так же для сохранения контекста безопасности между запросами можно использовать репозитории контекстов безопасности — SecurityContextRepository
, что также демонстрируется в примере кода фильтра.
В следующих статьях я буду более подробно разбирать способы аутентификации в Spring Security, а пока вы можете ознакомиться с роликами, посвящёнными Spring Security.
Понравилась статья? Тогда поддержки проект и подкинь монетку:
Больше полезных статей и роликов: