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 при его истечении.