Логотип Workflow

Article

Spring Security Fundamentals

Этап 6 — Spring Security: понять цепочку, чтобы перестало «ломаться всё»

Spring Security — это слой, который решает, кто отправил запрос и можно ли этому пользователю выполнить действие. Он стоит перед контроллерами, поэтому часто кажется, что «сломался endpoint», хотя метод контроллера вообще не был вызван. Чтобы Spring Security перестал выглядеть хаотично, нужно видеть путь запроса до контроллера.

Для новичка важно начать с простых вопросов. Пользователь вошёл в систему или прислал токен? Приложение смогло понять, кто это? Какие роли или права у этого пользователя? Разрешён ли ему конкретный URL или конкретная бизнес-операция? Spring Security отвечает на эти вопросы через filter chain, SecurityContext и правила доступа.

Authentication and authorization flow

Authentication и Authorization простыми словами

Authentication отвечает на вопрос «кто ты?». Например пользователь отправляет логин и пароль, session cookie или JWT token. Spring Security проверяет эти данные и, если всё хорошо, создаёт identity: principal и authorities. Principal — это текущий пользователь в системе. Authorities — это роли или права, например ROLE_ADMIN, ROLE_USER, ORDER_READ.

Authorization отвечает на вопрос «что тебе можно?». Пользователь может быть успешно authenticated, но всё равно не иметь доступа к ресурсу. Например обычный пользователь вошёл в систему, но пытается открыть admin endpoint. В этом случае identity известна, но доступ запрещён.

Разница между 401 и 403 строится именно на этом. 401 Unauthorized обычно означает, что пользователь не прошёл authentication: нет токена, токен сломан, session отсутствует, пароль неверный. 403 Forbidden означает, что пользователь уже понятен системе, но authorization не разрешает действие.

ТерминПростой смыслПример
CredentialsТо, чем пользователь доказывает личностьПароль, cookie, JWT
PrincipalПользователь после успешной проверки[email protected]
AuthoritiesРоли или права пользователяROLE_ADMIN, ORDER_WRITE
AuthenticationПроверка личностиВалидировать пароль или JWT
AuthorizationПроверка доступаМожно ли вызвать /admin/users

Зачем нужен Security Filter Chain

Spring Security работает через фильтры. Filter — это код, который выполняется до контроллера. В web-приложении запрос сначала проходит servlet filters, затем попадает в Spring MVC. Spring Security добавляет свою цепочку фильтров, где каждый фильтр отвечает за часть security-задач.

Один фильтр может обработать CORS, другой — CSRF, третий — попытаться прочитать session, четвёртый — проверить JWT, следующий — положить пользователя в SecurityContext, а затем отдельный слой решает, можно ли идти к endpoint. Если на любом шаге проверка не прошла, контроллер не вызывается. Ответ формируется раньше: например 401 или 403.

Security filter chain

Пример: frontend отправляет GET /api/orders с header Authorization: Bearer <token>. JWT filter достаёт token, проверяет подпись и срок действия, читает user id и роли. Если token валиден, Spring создаёт authentication object и кладёт его в SecurityContext. После этого authorization rules проверяют, разрешён ли пользователю доступ к /api/orders.

Что такое SecurityContext

SecurityContext — это место, где Spring Security хранит информацию о текущем authenticated user во время обработки запроса. Пока запрос выполняется, контроллеры, сервисы и @PreAuthorize могут узнать, кто пользователь и какие у него authorities.

Если authentication не прошла, в SecurityContext нет нормального пользователя. Если authentication прошла, там лежит объект authentication: principal, authorities и признак authenticated. В stateless JWT API этот context обычно создаётся заново на каждом запросе после проверки token. В session-based приложении context может восстанавливаться из session.

UserDetailsService и PasswordEncoder

Если приложение использует login/password, нужно где-то загрузить пользователя и безопасно проверить пароль. UserDetailsService отвечает за загрузку пользователя по username. Обычно он идёт в базу, находит запись пользователя и возвращает объект с username, hashed password и authorities.

PasswordEncoder нужен для проверки пароля. Пароли нельзя хранить plain text. В базе должен лежать hash, например BCrypt hash. Когда пользователь вводит пароль, Spring Security не расшифровывает старый пароль, а сравнивает введённое значение с hash через PasswordEncoder.matches.

@Service
class AppUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = userRepository.findByEmail(username)
            .orElseThrow(() -> new UsernameNotFoundException(username));

        return org.springframework.security.core.userdetails.User
            .withUsername(user.getEmail())
            .password(user.getPasswordHash())
            .authorities(user.getRole())
            .build();
    }
}
@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

Такой код нужен не для JWT-only API в каждом проекте, но он объясняет базовую модель Spring Security: найти пользователя, проверить credentials, собрать authorities.

Session vs Stateless JWT

Session-based security хранит состояние входа на сервере. После login сервер создаёт session, браузер получает cookie с session id, а на следующих запросах отправляет эту cookie. Сервер по session id находит пользователя. Это удобно для классических web-приложений, но требует server-side session storage.

Stateless JWT работает иначе. После login клиент получает token и отправляет его на каждом запросе. Сервер не ищет session, а проверяет сам token: подпись, срок действия, issuer, user id, roles или claims. Это удобно для API и горизонтального масштабирования, но усложняет logout, refresh tokens, revoke и key rotation.

JWT не является «просто строкой с user id». Если подпись не проверяется, срок не проверяется или роли читаются неправильно, безопасность ломается. Поэтому JWT filter должен строго валидировать token и правильно превращать claims в GrantedAuthority.

Как обычно выглядит SecurityConfig

В Spring Boot security-настройка часто задаётся через SecurityFilterChain bean. Здесь описывают, какие endpoints открыты, какие требуют authentication, где включается CORS/CSRF, какой режим session используется и какие custom filters добавляются.

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
        .csrf(csrf -> csrf.disable())
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/auth/**").permitAll()
            .requestMatchers("/api/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated())
        .build();
}

Этот пример означает: auth endpoints открыты, admin endpoints требуют role ADMIN, остальные запросы требуют authenticated user. Для stateless JWT обычно ещё добавляют custom JWT filter перед стандартным username/password filter.

Важно не копировать конфигурацию без понимания. Например csrf.disable() может быть нормальным для stateless token API, но опасным для cookie/session приложения. permitAll() полезен для login endpoint, но опасен для endpoints с приватными данными.

@PreAuthorize и method-level security

URL-правила защищают маршруты, но иногда этого мало. Один endpoint может работать с разными объектами, где доступ зависит от владельца ресурса или business rule. Например пользователь может читать только свои заказы, а admin — любые.

@PreAuthorize ставится на метод сервиса или контроллера и проверяет правило перед выполнением метода. Для этого нужно включить method security, обычно через @EnableMethodSecurity.

@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public List<OrderDto> getOrdersForUser(Long userId) {
    return orderRepository.findByUserId(userId);
}

Такое правило ближе к бизнес-операции. Даже если кто-то случайно откроет URL шире, критичная проверка останется около метода, который реально выдаёт данные.

Практический пример

Пользователь открывает страницу заказов, frontend отправляет GET /api/orders с JWT. Запрос сначала проходит CORS и другие filters. JWT filter читает header Authorization, достаёт token, проверяет подпись и срок. Если token невалиден, Spring Security возвращает 401, и controller не вызывается.

Если token валиден, filter создаёт authentication и кладёт его в SecurityContext. Теперь Spring знает principal и authorities. Затем authorization rules проверяют endpoint: например /api/orders требует authenticated user. Если пользователь authenticated, запрос идёт в controller. Если endpoint требует ROLE_ADMIN, а у пользователя только ROLE_USER, будет 403.

В service method может быть дополнительная проверка через @PreAuthorize: пользователь может читать только свои заказы. Это защищает не просто URL, а саму бизнес-операцию.

Итог этапа

Spring Security не «ломает всё» случайно. Он выполняет проверки раньше контроллера. Authentication создаёт identity, SecurityContext хранит её на время запроса, authorization решает доступ, а filter chain может остановить запрос до MVC. Если разбирать проблему по шагам: credentials -> authentication -> SecurityContext -> authorities -> authorization rule, поведение становится понятным.

Чек-лист понимания

  • Могу объяснить разницу между authentication и authorization.
  • Понимаю, почему 401 и 403 означают разные проблемы.
  • Понимаю, что Security filter chain выполняется до контроллера.
  • Могу объяснить, что хранится в SecurityContext.
  • Понимаю роль UserDetailsService и PasswordEncoder.
  • Могу сравнить session-based подход и stateless JWT.
  • Понимаю, зачем нужны URL rules и @PreAuthorize.

Quiz

Проверьте, что вы усвоили

Авторизуйтесь чтоб пройти тесты