Spring Security — это главный фреймворк для защиты приложений на платформе Spring. Он отвечает за:
-
Аутентификацию (Authentication) — проверку личности пользователя (логин/пароль, OAuth2-токен, JWT).
-
Авторизацию (Authorization) — проверку прав доступа: что конкретный пользователь может делать в системе.
-
Защиту от атак: CSRF (подделка запросов), session fixation (фиксация сессии), clickjacking, brute-force и т. д.
Почему это важно?
-
Современные приложения практически всегда обрабатывают персональные или корпоративные данные.
-
Уязвимость в безопасности = прямые убытки, утечки и репутационный удар.
-
Spring Security считается стандартом: на сегодняшний день подавляющее большинство продакшн-приложений на Spring используют его.
Spring Security встроен глубоко в экосистему Spring Boot: достаточно добавить зависимость spring-boot-starter-security, и по умолчанию всё приложение защищено (доступ разрешён только аутентифицированным пользователям).
Основная задача разработчика — настроить и расширить базовую защиту под конкретные требования: web-приложение с формой логина, REST API с JWT, микросервисы с OAuth2 и т. д.
2. Архитектура Spring Security
Архитектура Spring Security построена на цепочке фильтров (Filter Chain).
2.1. Как проходит запрос
-
Клиент (браузер, мобильное приложение, другой сервис) формирует HTTP-запрос.
-
Если включён HTTPS — трафик шифруется.
-
Запрос может содержать
Authorization: Bearer <token>илиCookie: JSESSIONID.
-
-
Серверное приложение (Spring Boot) получает запрос.
-
До контроллеров запрос попадает в цепочку фильтров (Filter Chain).
-
-
Spring Security Filter Chain проверяет:
-
аутентифицирован ли пользователь?
-
какие права у него есть?
-
разрешён ли доступ к ресурсу?
-
-
Если проверка не пройдена → ответ 401 (Unauthorized — нет аутентификации) или 403 (Forbidden — нет прав).
-
Если проверка успешна → запрос уходит в контроллер, сервис, БД.
2.2. Ключевые элементы
-
DelegatingFilterProxy — точка входа в Spring Security из контейнера сервлетов (Tomcat, Jetty и т. д.).
-
FilterChainProxy — управляет списком фильтров.
-
SecurityFilterChain — конфигурация безопасности для конкретного набора URL (например,
/api/**защищается через JWT, а/loginработает через форму). -
AuthenticationManager — главный менеджер, который отвечает за аутентификацию. Он делегирует проверку конкретным провайдерам (
AuthenticationProvider). -
UserDetailsService — интерфейс, который загружает информацию о пользователе из БД (логин, пароль, роли).
-
PasswordEncoder — отвечает за хэширование и проверку паролей (BCrypt и т. д.).
-
SecurityContext — объект, где хранится информация о текущем пользователе (
Authentication).
2.3. Пример реальной цепочки фильтров
В приложении с формой логина Spring Security строит цепочку фильтров, которая включает:
-
SecurityContextPersistenceFilter— загружает данные о пользователе из сессии. -
CsrfFilter— проверяет CSRF-токен. -
UsernamePasswordAuthenticationFilter— перехватывает POST/login, проверяет логин/пароль. -
BasicAuthenticationFilter— обрабатывает заголовокAuthorization: Basic .... -
BearerTokenAuthenticationFilter— проверяет JWT или OAuth2-токен. -
AnonymousAuthenticationFilter— если пользователь не залогинен, присваивает ему "анонимную" роль. -
ExceptionTranslationFilter— обрабатывает ошибки безопасности. -
FilterSecurityInterceptor— финальная проверка прав на доступ.
2.4. Servlet vs Reactive
-
Servlet (Tomcat/Jetty/Undertow): используется классическая цепочка фильтров (
Filter). -
Reactive (WebFlux/Netty): применяется
WebFilter,SecurityWebFilterChain, реактивныйReactiveAuthenticationManager. Отличие — контекст безопасности хранится в reactive context, а не вThreadLocal.
3. Аутентификация и Авторизация
3.1. Аутентификация (Authentication)
Аутентификация = процесс проверки личности пользователя.
Spring Security отвечает на вопрос: кто выполняет запрос?
Основные сценарии:
-
Форма логина (Form Login)
-
Пользователь отправляет POST
/loginсusernameиpassword. -
UsernamePasswordAuthenticationFilterперехватывает запрос. -
AuthenticationManagerвызываетDaoAuthenticationProvider. -
UserDetailsServiceищет пользователя в БД. -
PasswordEncoderсверяет пароль (BCrypt, Argon2). -
Если проверка успешна → создаётся объект
Authentication, который сохраняется вSecurityContext.
-
-
HTTP Basic
-
Запрос содержит
Authorization: Basic base64(user:password). -
Применяется в простых REST API (только через HTTPS).
-
-
JWT (Bearer Token)
-
В заголовке:
Authorization: Bearer <jwt>. -
Токен проверяется (
exp,iss,aud, подпись). -
Если валидный → создаётся
Authentication. -
Stateful-сессий нет, каждая проверка независимая.
-
-
OAuth2 / OpenID Connect
-
Пользователь логинится через внешнего провайдера (Google, GitHub).
-
Spring Security получает access token / id token.
-
Ресурсный сервер (resource server) проверяет токен (обычно JWT).
-
-
LDAP / Active Directory
-
Проверка пользователей через корпоративные директории.
-
-
Кастомная аутентификация
-
Можно написать свой
AuthenticationProvider. -
Например, проверка HMAC-подписи запроса или API-ключей.
-
3.2. Авторизация (Authorization)
Авторизация = проверка прав доступа.
Spring Security отвечает: какие действия разрешены?
Уровни:
-
URL-уровень
В конфигурацииHttpSecurity:http.authorizeHttpRequests(auth -> auth .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") .anyRequest().authenticated() );/admin/**доступен только ADMIN,/user/**доступен USER и ADMIN, остальные URL требуют аутентификации. -
Методный уровень
Используются аннотации в сервисах:@PreAuthorize("hasRole('ADMIN')") public void deleteUser(Long id) { ... }Или более сложные выражения (SpEL):
@PreAuthorize("#id == principal.id or hasRole('ADMIN')") public Account getAccount(Long id) { ... }Таким образом проверка может быть завязана не только на роли, но и на бизнес-логику.
-
Доступ на уровне доменных объектов (ACL)
-
Используется
AclServiceиPermissionEvaluator. -
Пример: только автор статьи может её редактировать.
-
3.3. UserDetails и PasswordEncoder
-
UserDetailsService
Интерфейс, который возвращает объектUserDetails.Пример кастомной реализации:
@Service public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public UserDetails loadUserByUsername(String username) { UserEntity user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("Not found")); return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), user.getRoles().stream().map(SimpleGrantedAuthority::new).toList() ); } } -
PasswordEncoder
Пароли никогда не хранятся в открытом виде.-
BCryptPasswordEncoder(по умолчанию). -
Argon2PasswordEncoder(ещё более современный).
Пример:
@Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }При регистрации пароль хэшируется, при логине — сравнивается хэш.
-
3.4. Почему это безопасно?
-
TLS/HTTPS защищает от перехвата трафика (человек посередине не увидит пароль).
-
Пароли хэшируются (bcrypt/argon2): даже если украдут БД, пароль в открытом виде не получить.
-
Токены подписываются: JWT содержит цифровую подпись, которую нельзя подделать без приватного ключа.
-
Авторизация многоуровневая: даже если злоумышленник получил токен, доступ ограничен ролями и ACL.
-
Механизмы защиты от атак: CSRF-токены, сессионная изоляция, ограничение попыток входа.
Аутентификация отвечает на вопрос «кто выполняет запрос?».
Авторизация отвечает на вопрос «что разрешено делать?».
Вместе они образуют основу Spring Security.
4. Жизненный цикл запроса в Spring Security
Чтобы уверенно работать со Spring Security, полезно понимать: что происходит, когда клиент делает запрос к приложению.
4.1. Общая последовательность
-
Клиент (браузер, мобильное приложение, другой сервис) → отправляет запрос (GET/POST/PUT и т. д.).
-
Запрос попадает на сервер (Tomcat/Jetty/Undertow).
-
Контейнер запускает цепочку фильтров (Filter Chain).
-
Среди фильтров есть
DelegatingFilterProxy, который передаёт управление в Spring Security. -
Запрос проходит через серию фильтров Spring Security.
-
Если аутентификация и авторизация успешные → запрос доходит до контроллера.
-
Контроллер выполняет бизнес-логику и возвращает ответ.
-
Ответ снова проходит через фильтры (часть данных сохраняется в сессию, добавляются заголовки безопасности).
4.2. Детально: шаг за шагом
Предположим, используется Spring Boot MVC + Form Login.
Шаг 1. Сетевой уровень
-
Клиент открывает страницу
/login. -
TLS/HTTPS устанавливает зашифрованное соединение.
-
Tomcat принимает запрос и превращает его в
HttpServletRequest.
Шаг 2. Контейнер запускает фильтры
-
Tomcat вызывает все фильтры, зарегистрированные в приложении.
-
Среди них есть
DelegatingFilterProxy("springSecurityFilterChain").
Шаг 3. Вход в Spring Security Filter Chain
-
FilterChainProxyопределяет, какие фильтры применяются для данного URL. -
Например, для
/loginсработаетUsernamePasswordAuthenticationFilter.
Шаг 4. Работа фильтров
Примерный порядок фильтров (по умолчанию):
-
SecurityContextPersistenceFilter
ЗагружаетSecurityContextиз сессии (если есть).
Если нет — создаёт новый (анонимный). -
CsrfFilter
Проверяет наличие CSRF-токена (для POST/PUT/DELETE). -
LogoutFilter
Перехватывает/logout, удаляет сессию, куки. -
UsernamePasswordAuthenticationFilter
Если POST/login→ берётusernameиpassword.
Создаёт объектUsernamePasswordAuthenticationToken.
Отправляет его вAuthenticationManager. -
AuthenticationManager→AuthenticationProvider
Например,DaoAuthenticationProvider.
Загружает пользователя черезUserDetailsService.
Сравнивает пароль черезPasswordEncoder.
Если проверка успешна → возвращаетAuthenticationс ролями. -
SecurityContextHolder
СохраняетAuthentication(вThreadLocal).
Если приложение stateful → данные пишутся вHttpSession. -
FilterSecurityInterceptor
Финальная проверка: есть ли у пользователя доступ к URL/методу.
ИспользуетAccessDecisionManager.
Если где-то возникает ошибка (например, пароль неверный, токен невалидный) → выбрасывается AuthenticationException, и клиент получает 401/403.
Шаг 5. Контроллер
-
Если проверки пройдены,
DispatcherServletвызывает контроллер. -
Например:
@GetMapping("/admin") @PreAuthorize("hasRole('ADMIN')") public String adminPage() { ... } -
Если пользователь имеет роль
ADMIN→ метод выполнится.
Шаг 6. Обратный путь (Response)
-
Контроллер возвращает
ResponseEntityилиView. -
Ответ снова проходит через фильтры (например,
HeaderWriterFilterдобавляет заголовки безопасности). -
Tomcat отправляет ответ клиенту.
4.3. Вариации
-
JWT API (stateless)
НетHttpSession.
BearerTokenAuthenticationFilterпроверяет токен в каждом запросе.
Контекст создаётся заново каждый раз. -
WebFlux (reactive)
ВместоFilterиспользуетсяWebFilter.
ВместоThreadLocal SecurityContextHolder→ реактивныйContext.
Вся работа выполняется асинхронно.
4.4. Ключевой принцип
Вся безопасность в Spring Security реализуется до выполнения бизнес-логики.
Если пользователь не прошёл проверку — контроллер даже не вызовется.
-
Spring Security работает как прослойка между клиентом и контроллером.
-
Каждый запрос проходит через цепочку фильтров.
-
Аутентификация и авторизация выполняются ДО бизнес-логики.
5. Stateful vs Stateless (сессии и JWT)
Один из ключевых вопросов в безопасности: как хранить и проверять информацию о пользователе между запросами.
5.1. Stateful (с состоянием, через сессии)
Как работает
-
Пользователь проходит аутентификацию (например, через форму).
-
Сервер проверяет логин/пароль.
-
При успешной аутентификации сервер сохраняет информацию о пользователе в HttpSession.
-
Это объект в памяти сервера.
-
В нём хранится
SecurityContextсAuthentication.
-
-
Клиенту отправляется cookie (JSESSIONID).
-
При каждом запросе клиент передаёт cookie, сервер по нему восстанавливает пользователя из сессии.
Пример
-
Первый вход →
POST /login -
Ответ сервера →
Set-Cookie: JSESSIONID=abc123 -
Дальнейшие запросы →
Cookie: JSESSIONID=abc123
Плюсы
-
Простая модель.
-
Безопасно для закрытых корпоративных приложений.
-
Работает «из коробки» в Spring Security.
Минусы
-
Масштабирование сложнее: при большом числе серверов необходимо шарить сессии (sticky sessions, Redis, Hazelcast).
-
Неудобно для REST API и микросервисов.
-
Плохо подходит для mobile-first и SPA (React, Angular, iOS/Android).
5.2. Stateless (без состояния, через JWT или токены)
Как работает
-
Пользователь аутентифицируется (или получает токен через OAuth2).
-
Сервер не хранит состояние в памяти.
-
Клиент получает JWT (JSON Web Token).
-
JWT хранится на стороне клиента (localStorage, secure cookie, mobile keystore).
-
При каждом запросе клиент передаёт заголовок:
Authorization: Bearer <jwt-token> -
Сервер проверяет токен (подпись, срок действия, права).
JWT структура
JWT состоит из трёх частей (разделённых точками):
header.payload.signature
-
Header → алгоритм подписи (например, HS256).
-
Payload → данные:
sub(user id),roles,exp(срок жизни). -
Signature → проверка подлинности (HMAC или RSA).
Пример payload
{
"sub": "user123",
"roles": ["USER"],
"exp": 1736448900
}
Плюсы
-
Простое масштабирование: сервер stateless, количество инстансов не ограничено.
-
Удобно для микросервисов.
-
Подходит для любых клиентов (web, mobile, API).
Минусы
-
JWT нельзя отозвать до истечения срока (если не хранить blacklist).
-
Payload доступен в открытом виде (Base64, не зашифрован, только подписан).
-
Требуется дополнительная схема для refresh-токенов.
5.3. Refresh токены
Чтобы access-токен не был «вечным», часто используется схема с двумя токенами:
-
Access Token — короткоживущий (например, 15 минут).
-
Refresh Token — долгоживущий (например, 30 дней).
Алгоритм:
-
Клиент получает оба токена при логине.
-
Access Token используется в запросах.
-
Когда срок действия истекает → клиент отправляет Refresh Token на
/refresh. -
Сервер выдаёт новый Access Token.
5.4. Spring Security реализация
-
Stateful (Session):
-
HttpSessionSecurityContextRepository -
Работает «по умолчанию» при form login.
-
-
Stateless (JWT):
-
BearerTokenAuthenticationFilter -
Настройка в
SecurityFilterChain:http .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .oauth2ResourceServer().jwt();
-
5.5. Когда использовать что
-
Stateful (сессии):
-
Внутренние корпоративные приложения.
-
Небольшое количество серверов.
-
Пользователи работают только через браузер.
-
-
Stateless (JWT):
-
REST API для мобильных приложений.
-
Микросервисная архитектура.
-
Высокая нагрузка и требование к масштабируемости.
-
Stateful → сервер хранит состояние, проще, но хуже масштабируется.
Stateless → клиент хранит токен, подходит для API и микросервисов.
JWT — стандартное решение для распределённых систем, но не универсальная «панацея».
6. Пароли и PasswordEncoder — как хранить безопасно
Одна из самых частых ошибок начинающих разработчиков — хранение паролей в открытом виде (plain text) или использование простых хэшей вроде MD5/SHA1.
Spring Security решает эту задачу через механизм PasswordEncoder.
6.1. Почему нельзя хранить пароли в открытом виде
Пример таблицы пользователей:
|
id |
username |
password |
|---|---|---|
|
1 |
admin |
qwerty123 |
|
2 |
user |
123456 |
Проблемы:
-
Если база утечёт → можно сразу войти.
-
Пользователи часто используют один и тот же пароль в других сервисах.
-
Даже администраторы БД видят пароли, что создаёт риск инсайда.
6.2. Хэширование вместо хранения «как есть»
Хранить необходимо не пароль, а хэш.
Пример (BCrypt):
$2a$10$DowJHd/8DqzRzTQaI7Em5Ocu8l7v.8dxyl0nHKz3Oy4rQ0cl6iTga
Преимущества:
-
Невозможность «обратного разворота» (хэш — односторонняя функция).
-
Разные хэши для одинаковых паролей (из-за соли).
-
Замедление брутфорса (BCrypt специально «тормозной»).
6.3. Алгоритмы в Spring Security
Доступные современные алгоритмы:
-
BCrypt (
BCryptPasswordEncoder)-
Самый распространённый, используется по умолчанию.
-
Применяет соль + параметр сложности (cost).
-
-
Argon2 (
Argon2PasswordEncoder)-
Победитель Password Hashing Competition.
-
Устойчив к атакам с использованием GPU/ASIC.
-
-
PBKDF2 (
Pbkdf2PasswordEncoder)-
Медленный алгоритм на основе HMAC.
-
-
SCrypt (
SCryptPasswordEncoder)-
Альтернатива BCrypt, более затратный по памяти.
-
Не рекомендуется использовать:
-
MD5, SHA-1, SHA-256 → слишком быстрые, легко поддаются брутфорсу.
6.4. PasswordEncoder в коде
Spring рекомендует использовать делегирующий энкодер:
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
Он создаёт DelegatingPasswordEncoder, который по умолчанию = BCrypt, но поддерживает и другие алгоритмы.
Пример записи пароля в БД:
{bcrypt}$2a$10$DowJHd/8DqzRzTQaI7Em5Ocu8l7v.8dxyl0nHKz3Oy4rQ0cl6iTga
6.5. Проверка пароля
Алгоритм:
-
Пользователь вводит пароль.
-
PasswordEncoder.matches(rawPassword, storedHash)сравнивает введённый пароль и хэш. -
В Spring Security это происходит автоматически в
DaoAuthenticationProvider.
Пример:
String raw = "qwerty123";
String encoded = passwordEncoder.encode(raw);
boolean matches = passwordEncoder.matches(raw, encoded); // true
6.6. Best Practices
-
Использовать BCrypt или Argon2.
-
Минимальная длина пароля — 8–12 символов, лучше 12–16.
-
Ограничение числа попыток входа (защита от brute force).
-
Хранить только хэш, никогда не логировать «сырой» пароль.
-
Регулярно обновлять алгоритмы (например, миграция с BCrypt → Argon2).
-
Добавлять 2FA для критичных систем.
7. AuthenticationManager и UserDetailsService
Spring Security спроектирован так, чтобы аутентификацию можно было адаптировать под любые источники — SQL-БД, LDAP или OAuth2.
В основе лежат три ключевых компонента:
-
AuthenticationManager — главный «оркестратор» аутентификации.
-
AuthenticationProvider — конкретный исполнитель проверки (например, БД или JWT).
-
UserDetailsService — загрузчик информации о пользователе (обычно из БД).
7.1. Authentication (объект аутентификации)
При входе пользователя Spring Security формирует объект Authentication.
До проверки:
UsernamePasswordAuthenticationToken [Principal=admin, Credentials=123456, Authenticated=false]
После успешной проверки:
UsernamePasswordAuthenticationToken [Principal=User(admin,ROLE_ADMIN), Credentials=[PROTECTED], Authenticated=true]
Ключевые поля:
-
Principal→ информация о пользователе (обычно UserDetails). -
Credentials→ чем подтверждён вход (пароль, токен). -
Authorities→ права (ROLE_USER,ROLE_ADMIN).
7.2. AuthenticationManager
Интерфейс с единственным методом:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
Назначение: принять запрос на аутентификацию и вернуть результат (Authentication) или выбросить исключение.
7.3. AuthenticationProvider
AuthenticationManager делегирует проверку одному или нескольким AuthenticationProvider.
Примеры встроенных провайдеров:
-
DaoAuthenticationProvider→ проверка логина/пароля черезUserDetailsService. -
LdapAuthenticationProvider→ проверка в LDAP/Active Directory. -
JwtAuthenticationProvider→ проверка JWT-токена.
7.4. UserDetailsService
Интерфейс для загрузки пользователя из источника (например, БД):
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Пример реализации (через JPA):
@Service
public class MyUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword()) // уже захэшированный пароль!
.roles(user.getRole())
.build();
}
}
7.5. Как работает связка
-
Клиент отправляет
POST /loginс логином и паролем. -
Фильтр
UsernamePasswordAuthenticationFilterперехватывает запрос и создаётUsernamePasswordAuthenticationToken. -
AuthenticationManagerпередаёт токен вDaoAuthenticationProvider. -
DaoAuthenticationProvider:-
вызывает
UserDetailsService.loadUserByUsername(username) -
получает пользователя из БД
-
сравнивает пароль через
PasswordEncoder.matches()
-
-
При успехе возвращается
AuthenticationсAuthenticated=true. -
SecurityContextHolderсохраняет пользователя, и он доступен в контроллерах через@AuthenticationPrincipal.
7.6. Пример SecurityConfig
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
return http
.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder)
.and()
.build();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}
7.7. Best Practices
-
Всегда использовать
PasswordEncoderпри загрузке пользователей. -
Отдельные таблицы пользователей и ролей (user/role).
-
Для кастомной логики (например, логин по email или номеру телефона) реализовать свой
UserDetailsService. -
При необходимости подключать несколько
AuthenticationProvider(например, для БД и JWT одновременно).
-
AuthenticationManager— центральная точка аутентификации. -
AuthenticationProvider— конкретный механизм проверки. -
UserDetailsService— загрузка пользователя из источника данных. -
В связке они дают гибкую систему аутентификации в Spring Security.
8. SecurityContext и SecurityContextHolder
Когда пользователь прошёл аутентификацию, Spring Security должен где-то хранить информацию о нём, чтобы:
-
не проверять пароль заново на каждый запрос,
-
быстро понимать, кто сделал запрос,
-
проверять права доступа в контроллерах и сервисах.
Для этого используется связка:
-
SecurityContext — контейнер, где лежит информация о текущем пользователе.
-
SecurityContextHolder — утилита, которая даёт доступ к текущему
SecurityContext.
8.1. SecurityContext
SecurityContext — это объект, который хранит только одну вещь: Authentication.
Пример:
SecurityContext context = SecurityContextHolder.getContext();
Authentication auth = context.getAuthentication();
Внутри Authentication лежит:
-
Principal— пользователь (обычно UserDetails). -
Authorities— список ролей/прав (ROLE_USER,ROLE_ADMIN). -
Details— доп. данные (например, IP, sessionId).
8.2. SecurityContextHolder
Это глобальное хранилище, через которое всегда получаем доступ к текущему пользователю.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
SecurityContextHolder обычно работает через ThreadLocal:
-
Для каждого запроса создаётся отдельный поток (в servlet-модели).
-
В этом потоке хранится
SecurityContext. -
Когда запрос завершился → контекст очищается.
8.3. Варианты хранения (Modes)
Spring Security поддерживает несколько стратегий хранения контекста:
-
MODE_THREADLOCAL (по умолчанию)
-
Каждый поток имеет свой SecurityContext.
-
Самый популярный вариант.
-
-
MODE_INHERITABLETHREADLOCAL
-
Дочерние потоки наследуют контекст родителя.
-
Нужно редко (например, при async-задачах).
-
-
MODE_GLOBAL
-
Один общий контекст для всех.
-
Почти никогда не используется (небезопасно).
-
8.4. SecurityContextPersistenceFilter
Кто вообще сохраняет и достаёт SecurityContext?
Это делает фильтр SecurityContextPersistenceFilter:
-
В начале запроса он загружает контекст (из сессии или создаёт новый).
-
В конце запроса он сохраняет обновлённый контекст обратно.
Для stateless (JWT) — новый контекст создаётся на каждый запрос.
Для stateful (сессии) — контекст достаётся из HttpSession.
8.5. Доступ к пользователю в контроллерах
Spring Security даёт несколько способов получить текущего юзера:
Через Authentication
@GetMapping("/me")
public String me(Authentication auth) {
return "Hello " + auth.getName();
}
Через SecurityContextHolder
@GetMapping("/me")
public String me() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return "Hello " + auth.getName();
}
Через аннотацию @AuthenticationPrincipal
@GetMapping("/me")
public String me(@AuthenticationPrincipal UserDetails user) {
return "Hello " + user.getUsername();
}
8.6. Пример в сервисах
Можно использовать пользователя не только в контроллерах, но и в сервисах:
@Service
public class AccountService {
public void doSomething() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String user = auth.getName();
System.out.println("Action by: " + user);
}
}
8.7. Особенности в WebFlux (реактивных приложениях)
В реактивных приложениях нет ThreadLocal, поэтому SecurityContextHolder не работает напрямую.
Вместо этого используется ReactiveSecurityContextHolder, который хранит контекст в Reactor Context.
Пример:
Mono<String> currentUser = ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication().getName());
8.8. Best Practices
-
Использовать
@AuthenticationPrincipalдля чистоты кода в контроллерах. -
Избегать прямого вызова
SecurityContextHolderвезде — лучше в сервисах. -
В реактивных приложениях всегда использовать
ReactiveSecurityContextHolder. -
Для асинхронных задач (например,
@Async) → использоватьDelegatingSecurityContextExecutorService, чтобы прокидывать контекст в другие потоки.
-
SecurityContextхранит текущего пользователя. -
SecurityContextHolderдаёт доступ к нему. -
В Servlet (stateful/stateless) используется ThreadLocal, в WebFlux — Reactor Context.
-
После логина вся инфа о пользователе живёт именно тут.
9. Авторизация в Spring Security
Аутентификация отвечает на вопрос: «Кто это?», а авторизация — «Что этот пользователь может?».
Spring Security делает это через цепочку: Voters → AccessDecisionManager → фильтры / методы.
9.1. Основные понятия
Authentication (аутентификация)
-
Проверка личности пользователя (логин/пароль, токен, OAuth2).
Authorization (авторизация)
-
Проверка прав доступа (roles/permissions/authorities).
Principal
-
Представление пользователя внутри
Authentication(обычноUserDetails).
Authorities / Roles
-
Права пользователя. Например:
ROLE_USER,ROLE_ADMIN. -
Разница: роль — это упрощённое обозначение группы прав, authority — конкретное право (
READ_ACCOUNT).
AccessDecisionManager
-
Компонент, который принимает решение о доступе.
-
Делегирует голосование к Voters.
Voters
-
Голосуют за/против доступа.
-
Типы:
-
RoleVoter— проверяет роли. -
AuthenticatedVoter— проверяет, аутентифицирован ли пользователь. -
WebExpressionVoter— проверяет SpEL-выражения (hasRole('ADMIN')).
-
9.2. URL vs Методная авторизация
1. URL-уровень (HTTP запросы)
Пример конфигурации через DSL:
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
);
Как это работает:
-
FilterSecurityInterceptorполучает запрос иAuthentication. -
Определяет, какой URL затрагивает правило.
-
AccessDecisionManager вызывает Voters → решение (grant/deny).
-
Если deny → 403 Forbidden.
2. Методная безопасность
Spring позволяет ставить аннотации прямо на сервисы:
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) { ... }
-
Под капотом: AOP-прокси вызывает AccessDecisionManager.
-
Можно использовать сложные выражения:
@PreAuthorize("#userId == principal.id or hasRole('ADMIN')")
-
principal— текущий пользователь. -
#userId— параметр метода.
9.3. Expression Language (SpEL)
SpEL позволяет проверять условия на основе Authentication:
-
hasRole('ROLE_ADMIN')— проверка роли. -
hasAuthority('WRITE_PRIVILEGE')— проверка права. -
principal.username == 'bob'— доступ для конкретного пользователя. -
#param == authentication.name— сравнение с текущим именем.
9.4. Внутренний процесс принятия решений
-
Запрос дошёл до
FilterSecurityInterceptor(web) или метод вызван (service). -
Получаем
AuthenticationизSecurityContext. -
Определяем необходимые права (например, URL pattern или аннотация).
-
AccessDecisionManager вызывает все Voters.
-
Каждое голосование:
-
ACCESS_GRANTED→ плюс. -
ACCESS_DENIED→ минус. -
ABSTAIN→ игнор.
-
-
Итоговое решение зависит от стратегии:
-
AffirmativeBased — хотя бы один grant → доступ разрешён.
-
UnanimousBased — все должны дать grant → доступ разрешён.
-
ConsensusBased — большинство vote → grant.
-
9.5. Роли и authorities
-
Роль (
ROLE_...) — это грубая категоризация. -
Authority — конкретное право (
READ_ACCOUNT). -
В Spring Security
RoleVoterищет префиксROLE_. -
Можно назначать сразу несколько ролей/authorities пользователю.
Пример в UserDetails:
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER"),
new SimpleGrantedAuthority("READ_ACCOUNT"));
}
9.6. Аудит и логирование доступа
-
Spring Security умеет логировать: кто попытался, с какого IP, какой результат.
-
Используется для мониторинга и выявления подозрительной активности.
Пример:
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication().getName());
}
-
Можно подключить к
@CreatedByи@LastModifiedByв JPA.
9.7. Best Practices
-
Не используйте только URL-паттерны — всегда комбинируйте с методной безопасностью.
-
Для REST API: проверяйте authorities, а не только роли.
-
Для сложных политик: используйте SpEL expressions.
-
Минимизируйте публичный доступ —
permitAll()только там, где реально безопасно. -
Логируйте попытки доступа для аудита.
-
В реактивных приложениях используйте
ReactiveAuthorizationManager.
9.8. Практический пример
@RestController
@RequestMapping("/accounts")
public class AccountController {
@GetMapping("/{id}")
@PreAuthorize("#id == principal.id or hasRole('ADMIN')")
public Account getAccount(@PathVariable Long id) {
return accountService.getAccount(id);
}
@PostMapping("/")
@PreAuthorize("hasAuthority('WRITE_ACCOUNT')")
public Account createAccount(@RequestBody Account account) {
return accountService.createAccount(account);
}
}
-
На GET доступ имеют либо администраторы, либо владелец.
-
На POST — только те, у кого есть право
WRITE_ACCOUNT.
-
URL и методная авторизация — два уровня защиты.
-
AccessDecisionManager + Voters → решают, можно ли.
-
SpEL даёт гибкость для сложных правил.
-
Роли и authorities — разные понятия, но совместно работают для fine-grained access.
10. OAuth2, JWT и безопасные токены (Tokens)
Spring Security поддерживает OAuth2 и JWT с полным набором фильтров и компонентов. Это позволяет защищать API и микросервисы даже в stateless-сценариях (без серверных сессий).
10.1. Основные понятия
OAuth2 (Open Authorization 2.0)
Стандарт делегированной аутентификации. Позволяет сторонним клиентам получать доступ к ресурсам пользователя без передачи его пароля.
Основные роли:
-
Resource Owner — владелец ресурса (пользователь).
-
Client — приложение, запрашивающее доступ.
-
Authorization Server — сервер авторизации, выдающий токены.
-
Resource Server — защищённый API, проверяющий токены.
JWT (JSON Web Token)
Стандартизированный формат токена: header.payload.signature.
-
Подпись (HMAC, RS256, ES256) защищает от подделки.
-
Payload содержит claims:
sub(subject/пользователь),exp(срок действия),roles,iss(issuer),aud(audience). -
Stateless: сервер не хранит сессий, достаточно валидного токена.
Access Token
Короткоживущий токен, используемый для доступа к ресурсам.
Refresh Token
Долгоживущий токен, позволяющий получить новый access token без повторной аутентификации.
Bearer Token
Передаётся в заголовке:
Authorization: Bearer <token>
10.2. Почему токены безопасны
Даже при использовании HTTPS токены дают дополнительные гарантии:
-
Подпись — изменения payload приводят к недействительной подписи.
-
Срок жизни (
exp) — короткие токены ограничивают время атаки. -
Audience / Issuer (
aud/iss) — токен работает только в целевых сервисах. -
Refresh flow — refresh token хранится безопасно (например, в HttpOnly cookie).
-
Stateless — сервер не хранит токены, снижается риск компрометации сессий.
Пример JWT:
{
"header": {"alg": "RS256", "typ": "JWT"},
"payload": {"sub": "user123", "roles": ["ROLE_USER"], "exp": 1700000000},
"signature": "abcdef123456..."
}
10.3. Spring Security Resource Server
Простейшая настройка JWT Resource Server:
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
return http.build();
}
-
BearerTokenAuthenticationFilterпроверяет токен в заголовке. -
Создаётся объект
Authenticationбез сессии (stateless).
10.4. OAuth2 Authorization Server
Если система сама выполняет функции сервера авторизации, используется проект Spring Authorization Server.
Поддерживаются flows:
-
Authorization Code (с PKCE для SPA).
-
Client Credentials (machine-to-machine).
-
Refresh Tokens.
Best practices:
-
хранение client secret в зашифрованном виде,
-
короткоживущие access tokens + refresh tokens в HttpOnly cookie,
-
настройка JWKs endpoint для проверки подписей.
10.5. Stateful vs Stateless
Stateful (сессии + cookies):
-
SecurityContextхранится вHttpSession. -
Logout — удаление сессии.
-
CSRF-защита необходима.
Stateless (JWT / Bearer):
-
Нет сессий → масштабируемо для микросервисов.
-
Logout реализуется удалением токена на клиенте.
-
CSRF не требуется, если токен в заголовке.
10.6. Refresh Tokens и защита
-
Access token: короткий срок, передаётся в заголовке.
-
Refresh token: долгий срок, хранится в HttpOnly cookie.
-
Flow: клиент отправляет refresh token → получает новый access token.
Пример endpoint для обновления:
@PostMapping("/token/refresh")
public JwtResponse refresh(@CookieValue("refreshToken") String token) {
Authentication auth = authService.verifyRefreshToken(token);
return authService.generateAccessToken(auth);
}
10.7. Проверка токена
Spring Security (с библиотекой Nimbus) проверяет:
-
подпись,
-
срок действия (
exp), -
издателя (
iss), -
аудиторию (
aud).
10.8. Типичные ошибки и защита
|
Ошибка |
Последствие |
Решение |
|---|---|---|
|
JWT без подписи |
Возможна подделка |
Использовать RS256/ES256 |
|
Долгоживущий access token |
Долгий доступ при краже |
Короткий TTL + refresh flow |
|
Refresh token доступен в JS |
XSS-уязвимость |
Хранение в HttpOnly cookie |
|
Access token в localStorage |
XSS → похищение |
Хранение в памяти (in-memory) |
|
Нет проверки audience |
Токен чужого сервиса |
Проверка |
10.9. Практический пример JWT
-
Пользователь логинится → сервер выдаёт access и refresh токены.
-
Клиент хранит access token в памяти, refresh — в HttpOnly cookie.
-
Каждый запрос отправляется с
Authorization: Bearer <access_token>. -
Spring Security проверяет подпись и claims → создаётся
Authentication. -
При истечении access token клиент использует refresh token для обновления.
11. Регистрация и безопасное хранение паролей (Registration & Password Security)
11.1. Основные принципы
-
Пароли никогда не хранятся в открытом виде.
-
Используются адаптивные хеш-функции (BCrypt, Argon2, SCrypt).
-
Для каждого пользователя применяется уникальная соль (salt).
-
Передача паролей происходит только через HTTPS.
-
Желательно применять политику сложности (минимальная длина, обязательные символы, blacklist простых паролей).
11.2. Поток регистрации
-
Пользователь вводит имя и пароль.
-
Контроллер принимает данные.
-
Сервис проверяет уникальность логина/email.
-
Пароль хешируется через
PasswordEncoder. -
Данные сохраняются в БД.
11.3. Настройка PasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // strength=12
}
-
BCrypt — стандарт по умолчанию.
-
Argon2 — современный и устойчивый к GPU-атакам.
-
SCrypt — тоже вариант с высокой защитой от brute-force.
11.4. Пример UserService
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public User registerUser(String username, String rawPassword) {
if (userRepository.existsByUsername(username)) {
throw new IllegalArgumentException("Пользователь уже существует");
}
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(rawPassword));
user.setRoles(Set.of("ROLE_USER"));
return userRepository.save(user);
}
}
11.5. Формы и CSRF
Для stateful приложений (сессии) CSRF-токен обязателен:
<form th:action="@{/register}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
</form>
11.6. Регистрация + Login + JWT
-
Регистрация → сохранение пользователя с хешированным паролем.
-
Login → проверка через
PasswordEncoder.matches(). -
При успехе создаётся
Authentication. -
Генерация JWT access token:
String token = jwtService.generateToken(authentication);
-
Все запросы к API защищаются через
SecurityFilterChainили@PreAuthorize.
11.7. Best practices
-
Rate-limiting для защиты от brute-force.
-
Подтверждение email перед активацией.
-
Пароли от 12+ символов, буквы, цифры, спецсимволы.
-
Lockout / throttling при множественных неудачных логинах.
-
Аудит логов для регистрации и входов.
11.8. Реактивный подход (WebFlux)
Используется ReactiveUserDetailsService и ReactiveSecurityContextHolder.
Пример:
Mono<UserDetails> findByUsername(String username) {
return userRepository.findByUsername(username)
.map(user -> User.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.build());
}
11.9. Полный поток (Registration → Login → JWT)
-
Регистрация: пользователь создаётся с хешированным паролем.
-
Login: проверка пароля.
-
Успешная аутентификация → генерация JWT.
-
API-защита через
BearerTokenAuthenticationFilter. -
Refresh flow обновляет access token при его истечении.
Автор: Zavik001
