Spring Security

«В интернете есть множество статей про данный фреймворк Spring, но хочется рассказать о нем простыми словами, без сложных фраз, чтобы любой новичок мог легко разобраться», — рассказывает мой коллега Денис.

Денис

Разработчик Java компании Programming store

Spring Security – это мощный и важный фреймворк в Spring
для обеспечения безопасности приложения при помощи аутентификации
и авторизации. Основан на цепочке фильтров. Например, в Spring Security 6.5.1.
по умолчанию стандартная цепочка фильтров (FilterChainProxy) содержит
до 12-16 фильтров, выполняющих различные механизмы проверки.
Стоит отметить, что точное количество фильтров зависит от конфигурации.

Стандартная цепочка фильтров
Стандартная цепочка фильтров

1. WebAsyncManagerIntegrationFilter – интеграция с асинхронными запросами.

2.       SecurityContextHolderFilter – сохранение информации аутентификации в SecurityContextHolder между запросами.

3.       HeaderWriterFilter – добавление HTTP-заголовков безопасности (X-Content-Type-Options, X-Frame-Options и др.).

4.       CsrfFilter – защита от CSRF-атак (если включена).

5.       LogoutFilte – обработка выхода пользователя (/logout).

6.       UsernamePasswordAuthenticationFilter – аутентификация по логину и паролю (если включена форма входа).

7.       DefaultLoginPageGeneratingFilter – генерация страницы входа по умолчанию (если не настроена кастомная).

8.       DefaultLogoutPageGeneratingFilter – генерация страницы выхода по умолчанию.

9. BasicAutenticationFilter – реализация базовой аутентификации HTTP (если используется HTTP Basic Auth)

10.     RequestCacheAwareFilter – сохранение запроса, требующего аутентификации (для редиректа после входа).

11.     SecurityContextHolderAwareRequestFilter – интеграция SecurityContext с сервлетами.

12.     AnonymousAuthenticationFilter – добавление анонимного пользователя, если никто не аутентифицирован.

13.     SessionManagementFilter – управление сессиями (например, ограничение количества сессий).

14.     ExceptionTranslationFilter – обработка исключений аутентификации и авторизации.

15.     FilterSecurityInterceptor  – финальный фильтр, проверяющий доступ к ресурсам.

16. AuthorizationFilter (появился в 6 версии) – замена часть логики в FilterSecurityInterceptor.

Аутентификация – процесс проверки пользователя (логин-пароль).

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

Данный фреймворк также позволяет защитить наше приложение от несанкционированных атак, таких как XSS, CSRF, Session Fixation, SQL-инъекций, Clickjacking, Brute Force и др.

XSS (Cross-Site Scripting) – атака с использованием межсайтового скриптинга, т.е. злоумышленник может через внедрение кода в браузере или через внедрение кода на сервере получить доступ к содержимому всех страниц и выполнять действия (например, http-запросы) под учетными записями пользователей.

CSRF (Cross-Site Request Forgery) – атака, при которой злоумышленник заставляет пользователя выполнить нежелательное действие на сайте, на котором тот уже аутентифицирован, т.е. межсайтовая подделка запроса.

Session Fixation – атака, при которой злоумышленник фиксирует ID сессии жертвы.

SQL-инъекции – внедрение вредоносного SQL-кода в запросы.

Clickjacking – подмена интерфейса, когда пользователь думает, что кликает на одном сайте, а на самом деле взаимодействует с другим.

Brute Force – подбор паролей или токенов.

Основные классы и интерфейсы Spring Security
Основные классы и интерфейсы Spring Security

SecurityContextHolder – класс, где хранится информация о том, кто аутентифицирован (подробная информация о пользователе). Также это класс, хранящий по умолчанию в ThreadLocal переменной SecurityContext-ы (текущие контексты безопасности) для каждого потока и содержащий статические методы для работы с SecurityContext-ами, а через них с текущим объектом Authentication, привязанным к нашему веб-запросу.

SecurityContext – интерфейс, отражающий контекст безопасности для текущего потока. Содержит объект Authentication (аналог ApplicationContext, в котором находятся бины). По умолчанию на каждый поток создается один SecurityContext. Имеет два метода — getAuthentication() и setAuthentication(Authentication authentication).

Authentication – интерфейс, содержащий информацию о текущем пользователе и его разрешениях (Principal). Работа Spring Security заключается в том, что различные фильтры и обработчики будут брать и класть объект Authentication для каждого пользователя. Authentication имеет реализацию по умолчанию – класс UsernamePasswordAuthenticationToken, предназначенный для хранения логина, пароля и коллекции Authorities. В Authentication есть метод getPrincipal(), возвращающий Object. При аутентификации с использованием имени пользователя/пароля Principal реализуется объектом типа UserDetails.

Principal – интерфейс из пакета java.security, отражающий учетную запись пользователя. Это логин.

Credentials – любой Object; то, что подтверждает учетную запись пользователя. Это пароль.

Authorities – права доступа (роли и разрешения), которые назначаются пользователю и определяют, что может делать пользователь в приложении. Authorities хранятся в Authentication и проверяются Spring Security.

GrantedAuthority – интерфейс, который описывает одно право пользователя в Spring Security. Обычно используется через реализацию SimpleGrantedAuthority. Иными словами, это полномочия, представленные пользователю (уровни доступа или роли).

UserDetails – интерфейс, представляющий учетную запись пользователя. UserDetails выступает в качестве Principal, т.е. как представление текущего аутентифицированного пользователя.

Как правило, модель пользователя должна имплементировать его. Она просто хранит пользовательскую информацию в виде логина, пароля и флагов isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired, isEnabled, а также коллекции прав (ролей) пользователя. Данная информация позже инкапсулируется в объекты Authentication в случае успешной авторизации.

UserDetailsService – интерфейс объекта, реализующего загрузку пользовательских данных из хранилища. Созданный нами объект с этим интерфейсом должен обращаться к БД и получать оттуда юзеров. Единственный метод этого интерфейса принимает имя пользователя в виде String и возвращает UserDetails. Он представляет собой Principal, но в расширенном виде и с учетом специфики приложения.

AuthenticationManager – основной стратегический интерфейс для аутентификации. Имеет только один метод authenticate(), который срабатывает, когда пользователь пытается аутентифицироваться в системе.

AuthenticationManager может сделать одну из 3 вещей в своем методе authenticate():

1. Вернуть Authentication (с authenticated = true), если предполагается, что вход осуществляет корректный пользователь.

2. Бросить AuthenticationException, если предполагается, что вход осуществляет некорректный пользователь.

3.  Вернуть null, если принять решение не представляется возможным.

ProviderManager – класс, который реализует AuthenticationManager, содержит поле private List<AuthenticationProvider> providers со списком AuthenticationProvider-ов и итерирует запрос аутентификации по этому списку AuthenticationProvider-ов. По очереди передает запрос каждому провайдеру из списка, пока один из них не сможет обработать запрос. Такое разделение позволяет поддерживать в одном приложении разные механизмы аутентификации (через БД, токены, LDAP, OAuth2.0).

AuthenticationProvider – интерфейс, определяющий объект, выполняющий аутентификацию. Он имеет множество готовых реализаций:

·       DaoAuthenticationProvider – аутентификация через UserDetailsService (например, БД),

·       LdapAuthenticationProvider – аутентификация через LDAP,

·       JwtAuthenticationProvider – аутентификация через JWT,

·       OAuth2AuthenticationProvider – аутентификация через OAuth2.0.

Также можно реализовать свой AuthenticationProvider. В небольших проектах обычно используется один провайдер (например, для аутентификации по логину и паролю), а в более крупных проектах – несколько провайдеров (например через Google, OAuth2, LDAP и т.д.), для каждого из которых создается свой объект AuthenticationProvider.

Интерфейс AuthenticationProvider похож на AuthenticationManager, но дополнительно содержит метод supports(Class<?> authentication), который позволяет определить, поддерживает ли данный провайдер конкретный тип объекта Authentication. Это позволяет гибко настраивать разные механизмы аутентификации в одном приложении.

PasswordEncoder – интерфейс для шифрования/расшифровывания паролей. Одна из популярных реализаций – BCryptPasswordEncoder.

Аутентификация пользователя происходит следующим образом:

1. Пользователь отправляет POST-запрос (например, на /login) с логином и паролем.

2. Фильтр UsernamePasswordAuthenticationFilter сначала перехватывает POST-запрос (например, на /login), далее получает имя пользователя, а также его пароль и создает экземпляр класса UsernamePasswordAuthenticationToken (объект Authentication).

3. Фильтр передает объект Authentication в AuthenticationManager для проверки. ProviderManager итерирует запрос аутентификации по этому списку AuthenticationProvider-ов, пока один из них не аутентифицирует пользователя.

4. AuthenticationProvider проверяет учётные данные для логина/пароля. Это, например, DaoAuthenticationProvider. Далее загружает UserDetails через UserDetailsService => cравнивает пароли (используя PasswordEncoder) => если аутентификация успешна, возвращает Authentication с Authorities, если нет,  выбрасывает AuthenticationException.

5. После успешной аутентификации SecurityContextHolder сохраняет Authentication в SecurityContext.

6. Если аутентификация успешна, пользователь перенаправляется на запрашиваемый ресурс. Если нет, возвращается ошибка (401 Unauthorized) или перенаправляет на страницу логина.

Аутентификация пользователя
Аутентификация пользователя

Авторизация пользователя происходит следующим образом:

1. Аутентифицированный пользователь отправляет HTTP-запрос к защищённому ресурсу.

2. Поскольку у нас пользователь аутентифицированный, фильтры, связанные с аутентификацией (UsernamePasswordAuthenticationFilter, BasicAuthenticationFilter), сработали на этапе входа: восстановили пользователя из сессии и поместили информацию о нём (Authentication) в SecurityContext.

3. Далее срабатывает AuthorizationFilter (фильтр авторизации) и использует AuthorizationManager для проверки доступа.

4. AuthorizationManager получает текущего пользователя (Authentication) из SecurityContextHolder. Сопоставляет запрошенный путь (или метод) с правилами доступа, заданными в конфигурации (например, через .authorizeHttpRequests()). Если правило разрешает доступ => запрос попадает в контроллер или другой обработчик, если же нет, фильтр возвращает ошибку 403 Forbidden.

Авторизация пользователя
Авторизация пользователя