Аутентификация в Spring Security

Процесс предоставления пользователю доступа к информационной системе состоит из трёх этапов: идентификации, аутентификации и авторизации.

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

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

  • Обычный пароль
  • Одноразовый пароль, сгенерированный по алгоритму 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, однако запрос и результат — разные объекты, содержащие разные данные:

  1. Метод getPrincipal() у запроса возвращает идентификатор пользователя (логин в случае с аутентификацией по логину и паролю), а у результата может возвращать как идентификатор, так и объект с подробными данными успешно аутентифицированного пользователя, например, UserDetails.
  2. Метод isAuthenticated() возвращает false у запроса и true — у успешного результата.

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

Интерфейс Authetication расширяет стандартный для JDK интерфейс Principal, добавляя методы, необходимые для работы Spring Security:

Для идентификации и аутентификации пользователей с использованием логина и пароля существует класс UsernamePasswordAuthenticationToken, расширяющий абстрактный класс AbstractAuthenticationToken:

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

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

Менеджер аутентификации

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

Задача данного компонента — обработать запрос на аутентификацию и вернуть успешный результат аутентификации, либо выбросить исключение. Впрочем, основная стандартная реализация этого интерфейса — ProviderManager не реализует процесс аутентификации, а делегирует большую его часть другим компонентам — провайдерам аутентификации, экземплярам классов, реализующих интерфейс AuthenticationProvider.

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

Провайдер аутентификации

Провайдеры аутентификации описываются интерфейсом AuthenticationProvider:

В процессе обработки запроса аутентификации провайдер должен получить данные о пользователе из некоторого источника, будь то база данных, сторонний сервис или параметры JWT, провалидировать с их помощью данные запроса и из них же заполнить свойства principalcredentials и authorities.

Каждый провайдер поддерживает какой-то специфичный способ аутентификации, так AbstractUserDetailsAuthenticationProvider поддерживает только UsernamePasswordAuthenticationToken. В контексте безопасности приложения может быть сконфигурировано несколько провайдеров для поддержки разных способов аутентификации. Более того, контекст безопасности приложения может содержать несколько провайдеров, способных обработать один и тот же тип запроса аутентификации. Это может быть полезно в случаях, когда провайдеры имеют разную логику обработки запроса аутентификации или источники данных о пользователях. В такой ситуации один провайдер может не аутентифицировать пользователя, а другой — успешно это сделать.

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

Данные о пользователе

Для создания корректного результата аутентификации необходимы данные о пользователе. В Spring Security для описания пользователя существует интерфейс UserDetails:

Стоит отметить, что Spring Security не взаимодействует с этим интерфейсом и его реализациями напрямую, следовательно, в качестве источника информации о пользователе может выступать любой класс или интерфейс. Единственное ограничение — класс или интерфейс, описывающий пользовательские данные, должен реализовывать или расширять интерфейс Serializable, так как пользовательские данные сериализуются при сохранении в HTTP-сессии.

Контекст безопасности

Контекст безопасности описывается интерфейсом SecurityContext и используется для хранения успешной аутентификации:

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

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

В следующих статьях я буду более подробно разбирать способы аутентификации в Spring Security, а пока вы можете ознакомиться с роликами, посвящёнными Spring Security.

Понравилась статья? Тогда поддержки проект и подкинь монетку:

Больше полезных статей и роликов: