itmo_conspects

Лекция 9

Spring Security

Spring Security – мощный фреймворк для обеспечения безопасности Java-приложений в экосистеме Spring. Он предоставляет комплексные сервисы безопасности (аутентификация, авторизация, фильтрация запросов и т.д.) для корпоративных веб-приложений. Spring Security возник из-за того, что встроенные в Java EE механизмы безопасности (Servlet/EJB) оказались слишком ограниченными и мало портируемыми для реальных задач. С помощью Spring Security можно гибко настроить проверку личности пользователя, разграничение прав доступа, защиту от CSRF, встроенную поддержку «remember-me», сессию, а также десятки других возможностей. Фреймворк автоматически интегрируется со Spring MVC/Boot и даже сам генерирует простые страницы логина/выхода по умолчанию

Основные понятия:


Spring Security работает с помощью фильтров. Фильтры последовательно обрабатывают пришедший HTTP-запрос и решают, обрабатывать его дальше или нет. Фильтры образуют цепочку обязанностей:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    @Bean
    @SneakyThrows
    public SecurityFilterChain securityFilterChain(HttpSecurity http) {
        return http
            .csrf().disable()
            .cors().disable()
            .authorizeHttpRequests(customizer -> customizer.anyRequest().authenticated())
            .httpBasic()
                .authenticationEntryPoint((request, response, authException) -> response.sendError(401))
            .and()
            .build();
    }
}

В этом примере мы:

  1. Отключаем для проекта защиту от атак CSRF (Cross-site request forgery). Атака CSRF работает так: вредоносный сайт отправляет от лица пользователя (сессия которого активна и находится в куки-файлах) POST-запрос, например, банку, а банк считает этот запрос валидным. Несмотря на то, что вредоносный сайт не видит куки, они автоматически прикрепляются в запросе браузером. Spring же для защиты от такого в запросе требует токен, который генерирует вместе с сессией
  2. Отключаем CORS (Cross-Origin Resource Sharing). По умолчанию браузер запрещают JavaScript-коду обращение к другим доменам, поэтому существует CORS: при запросе к отличающемуся домену сервер должен отправить в заголовке HTTP-запроса Access-Control-Allow-Origin: https://example.com. Тогда браузер обработает его
  3. Делаем так, что все запросы должны исходить от авторизованного пользователя. Иначе возвращаем 401 Unauthorized или перенаправляем на страницу с авторизацией
  4. Возвращаем настроенную цепочку фильтров

По умолчанию, фильтры такие:

  1. WebAsyncManagerIntegrationFilter -Интеграция безопасности с асинхронными запросами (например, @Async)
  2. SecurityContextPersistenceFilter - Загружает/сохраняет SecurityContext в HttpSession (или другую стратегию) устанавливает его в SecurityContextHolder для текущего потока. Если контекст ещё не создан (первый запрос), он создаёт новый «пустой» контекст. Это обеспечивает получение информации о ранее вошедшем пользователе
  3. HeaderWriterFilter - Добавляет HTTP-заголовки безопасности (например, X-Frame-Options)
  4. CsrfFilter - Обрабатывает защиту от CSRF-атак (если включена)
  5. LogoutFilter - Обрабатывает POST-запросы на /logout. В этом случае удаляется CSRF-токен, завершается сессия, чистится SecurityContextHolder
  6. BasicAuthenticationFilter - Обрабатывает HTTP Basic авторизацию (если используется): извлекает логин\пароль и передает их в AuthenticationManager AuthenticationManager представляет из себя интерфейс с одним методом:

     Authentication authenticate(Authentication authentication) throws AuthenticationException;
    
    

    В нашем случае имплементацией Authentication будет UsernamePasswordAuthenticationToken

    AuthenticationManager получает объект Authentication (например, с username и password) и передаёт его в подходящий AuthenticationProvider, чтобы проверить подлинность. AuthenticationProvider содержит в себе два метода:

     Authentication authenticate(Authentication authentication) throws AuthenticationException;
     boolean supports(Class<?> authentication);
    

    Обычно реализуется через ProviderManager, который внутри содержит список AuthenticationProvider‘ов.

    AuthenticationProvider проверяет, поддерживает ли он данный тип Authentication с помощью метода supports. Если поддерживает — проверяет credentials в методе authenticate (например, сверяет пароль и логин), а если аутентификация успешна — возвращает полностью заполненный объект Authentication с флагом authenticated=true.

    Если все провайдеры выкинул исключение, ProviderManager выбросит AuthenticationManager последнее исключение

    Далее фильтр сохраняет полученный Authentication в SecurityContextHolder. Если выбросится AuthenticationException от провайдеров, то будет сброшен контекст, и вызовется AuthenticationEntryPoint

  7. RequestCacheAwareFilter - Кэширует защищённые запросы, чтобы потом на них вернуть пользователя:
    1. Пользователь заходит на защищенный url.
    2. Его перекидывает на страницу логина.
    3. После успешной авторизации пользователя перекидывает на страницу которую он запрашивал в начале.
  8. SecurityContextHolderAwareRequestFilter - Делает request.isUserInRole(...) и getUserPrincipal() работающими
  9. AnonymousAuthenticationFilter - Назначает “анонимного пользователя”, если пользователь не аутентифицирован. Фильтр заполняет объект SecurityContextHolder анонимной аутентификацией AnonymousAuthenticationToken с ролью ROLE_ANONYMOUS. Это гарантирует что в SecurityContextHolder будет объект
  10. SessionManagementFilter - Производится действия, связанные с сессией. Это может быть:
    1. Смена идентификатора сессии
    2. Ограничения количества одновременных сессий
    3. Сохранение SecurityContext в securityContextRepository

    В обычном случае происходит следующее: SecurityContextRepository с дефолтной реализацией HttpSessionSecurityContextRepository сохраняет SecurityContext в сессию. Вызывается sessionAuthenticationStrategy.onAuthentication:

    1. По умолчанию, включена защита от Session Fixation Attack, то есть после аутентификации меняется идентификатор сессии.
    2. Если был передан CSRF-токен, генерируется новый CSRF-токен
  11. ExceptionTranslationFilter - Перехватывает исключения (например, недостаточно прав или ошибка аутентификации) из более глубоких фильтров. Если пользователь не аутентифицирован, ExceptionTranslationFilter перенаправит его на точку входа (например, на страницу логина) или вернёт HTTP 401/403. Этот механизм обеспечивает корректное реагирование на проблемы с безопасностью.
  12. FilterSecurityInterceptor - Проверяет соответствие между требуемыми правами доступа и правами текущего пользователя. Здесь происходит сравнение атрибутов защищаемого ресурса (например, требуемой роли) с полномочиями в Authentication. Если прав достаточно, запрос пропускается к контроллеру, иначе – генерируется отказ в доступе.

После обработки запроса всеми фильтрами результат либо отдается клиенту (запрос был разрешён), либо возвращается ошибка безопасности. При этом при завершении запроса SecurityContextPersistenceFilter может сохранить обновлённый SecurityContext (например, если пользователь только что вошёл) обратно в сессию.

Также есть:


Конфигурация Spring Security может выполняться чисто на Java (без XML). Современный подход – объявлять бины SecurityFilterChain и UserDetailsService или WebSecurityCustomizer. Начиная с Spring Security 5.7 класс WebSecurityConfigurerAdapter считается устаревшим, и вместо него рекомендуется использовать SecurityFilterChain. Например, класс конфигурации может выглядеть так:

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
            .httpBasic(Customizer.withDefaults());
        return http.build();
    }

    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
        return new InMemoryUserDetailsManager(user);
    }
}

В этом примере все запросы к приложению требуют аутентификации (anyRequest().authenticated()), и используется HTTP Basic для ввода логина/пароля. Пользователь с именем user, паролем password и ролью USER хранится в памяти внутри InMemoryUserDetailsManager. Метод withDefaultPasswordEncoder() упрощает демонстрацию (пароль автоматически кодируется), но не предназначен для продакшен-использования – в реальном приложении следует хранить пароль в безопасном зашифрованном виде.

При подобной конфигурации Spring Boot создаст простую стандартную форму логина/логаута по умолчанию. Как только разработчик добавляет Spring Security, по умолчанию все URL блокируются и требуют входа (пока не настроено иное поведение). Поведение фильтров (какие URL защищать, какие – открыты) меняется через методы authorizeHttpRequests(), permitAll(), hasRole() и т.д. Дополнительно можно настраивать CSRF-защиту, правила для статических ресурсов, перенаправления и прочее через объект HttpSecurity.

Если не настраивать пользователей вручную, Spring Boot по умолчанию создаст одного пользователя с именем user и сгенерированным паролем, который выводится в логи при запуске. Но обычно для учебных примеров и небольших приложений проще явно указать своих пользователей (как выше).


Продвинутые возможности: