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 – подбор паролей или токенов.

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.
