Pull to refresh

Spring security: без фильтров по умолчанию, как и что из этого получится

Reading time8 min
Views17K

Статья - краткое напоминание о:
- какие фильтры по-умолчанию уже встроены, и как их убрать или донастроить,
- список прочих доступных уже готовых к использованию фильтров и полный их список с порядком,
- даже слишком краткий обзор методов HttpSecurity, они могут быть очень странными,
- пример, как писать фильтры безопасности после Spring 5.7, когда привычный класс настроек устареет,
- подчеркнул откуда стоит начинать отладку, чтобы понять суть происходящего.


Введение

Решая тестовое задание, когда доступ к ресурсу предоставляется не всем, я выбрал Spring Security, чтобы помочь себе: использовать готовое, научиться интересным кодовым решениям, лучше разобраться в теме удостоверения и предоставления прав (authentication, authorization). Мне показалось неуютным, когда глядя на примеры в сети, очень многие примеры, я находил готовые решения без объяснения выбора, без понимания почему так, и каков контекст. Так остаётся бесконтрольным, к примеру, что сейчас подключено, а что нет. Лишнее в коде лучше не оставлять, тем более которое как-то работает, а тебе не известно как именно.

Зависимости Gradle
dependencies {
  // необходимые
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
      
  // для красоты, удобства и тестов
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

Настройка

Одним куском такую информацию так нигде и не встретил, пришлось поработать в ручную, "методом проб и ошибок". Итак, способы настройки, если игнорируете прелести Spring Boot: 1) через xml файл конфигурации, два других программные: 2) это через наследование классу WebSecurityConfigurerAdapter (с версии 5.7 признан устаревшим), и 3) через класс конфигурации. На оба рекомендовано навесить аннотацию EnableWebSecurity.

'@EnableWebSecurity',
@Retention(RetentionPolicy.RUNTIME) 
@Target({ElementType.TYPE}) 
@Documented 
@Import({org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration.class,org.springframework.security.config.annotation.web.configuration.SpringWebMvcImportSelector.class,org.springframework.security.config.annotation.web.configuration.OAuth2ImportSelector.class,org.springframework.security.config.annotation.web.configuration.HttpSecurityConfiguration.class}) 
@EnableGlobalAuthentication 
@Configuration 
public @interface EnableWebSecurity
extends annotation.Annotation

Она вбирает в себя и хорошо известную аннотацию @Configuration и @EnableGlobalAuthentication (помечает, что класс может быть использован для построения экземпляра AuthenticationManagerBuilder - строитель того, что используют фильтры, о которых идёт здесь речь).

Как только вы это сделали. Ну т.е. самый простой шаг

@EnableWebSecurity
public class SecurityFilterChainImpl {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .build();
    }
}

... оказывается, что на самом деле вы уже подключили 11 фильтров. Такой сюрприз. Мне это не показалось однозначно удобным. Сразу скажу, что все кроме одного можно легко выключить или перенастроить. WebAsyncManagerIntegrationFilter мне не поддался. А я очень старался. Вот некоторые подробности о каждом из них.

Филтры, подключенные по умолчанию:
org.springframework.security.web.authentication.       AnonymousAuthenticationFilter
org.springframework.security.web.csrf.                 CsrfFilter
org.springframework.security.web.session.              DisableEncodeUrlFilter
org.springframework.security.web.access.               ExceptionTranslationFilter
org.springframework.security.web.header.               HeaderWriterFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.savedrequest.         RequestCacheAwareFilter
org.springframework.security.web.servletapi.           SecurityContextHolderAwareRequestFilter
org.springframework.security.web.context.              SecurityContextPersistenceFilter
org.springframework.security.web.session.              SessionManagementFilter
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter

Если все эти фильтры выключить, то все ресурсы будут доступны для всех запросов. Ну т.е. проверки можно сказать теперь нет.

Выключить то, что уже настроено заранее можно так. В комментарии написан класс фильтра, который настраиваем данным методом. Соответствия имён методов именам классов иногда неожиданные.
import lombok.val;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity
public class SecurityFilterChainImpl {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .anonymous(AbstractHttpConfigurer::disable)         // AnonymousAuthenticationFilter
                .csrf(AbstractHttpConfigurer::disable)              // CsrfFilter
                .sessionManagement(AbstractHttpConfigurer::disable) // DisableEncodeUrlFilter, SessionManagementFilter
                .exceptionHandling(AbstractHttpConfigurer::disable) // ExceptionTranslationFilter
                .headers(AbstractHttpConfigurer::disable)           // HeaderWriterFilter
                .logout(AbstractHttpConfigurer::disable)            // LogoutFilter
                .requestCache(AbstractHttpConfigurer::disable)      // RequestCacheAwareFilter
                .servletApi(AbstractHttpConfigurer::disable)        // SecurityContextHolderAwareRequestFilter
                .securityContext(AbstractHttpConfigurer::disable)   // SecurityContextPersistenceFilter
                .build();
    }
}

Другой синтакис отключения Спрингом уже признаётся устаревшим и менее удобным, менее читаемым: без лямбд вот так .anonymous().disable().and()

Если сравните список методов настройки и связанные с ними классы, то обнаружите, что иногда даже отдалённо не напоминают названием имена методов настройки (см. код и комментарии, я нарочно разместил методы настроек и настраиваемые фильтры в том же порядке, про порядок речь также пойдёт ниже). Иногда один метод подключает сразу два фильтра. Когда всё лишнее отключили, кажется что это база, что такое решение независимо от других настроек, но это не так. Стоит вам защитить доступ хотя бы к одному ресурсу (из множеста адресов хотя бы один), то остальные станут тоже недоступны. А всё потому что окажется понадобится AnonymousAuthenticationFilter. Без него никак. Ещё странным кажется, что некоторым методам не соотвествует ни один фильтр: .userDetailsService() и .portMapper(). И вот приходится копаться. К нюансам можно привыкнуть, но время!..

Коротко про устройство системы с фильтрами. Фильтры, сколько бы их ни было, слагают шаблон "цепочка ответственности" косвенно рекурсивно вызывают один другого по порядку. У стандартных порядок предопределён (см. ниже), можно переопределить или добавить новые фильтры, указывая такой порядок для каждого. Все они будут встроены внутрь бина (боба по имени :-)) "springSecurityFilterChain":

Некоторые фильтры, предопределённые Спригом, и их порядковые номера

пакет

класс

порядковый номер

org.springframework.security.web.session.

DisableEncodeUrlFilter

100

org.springframework.security.web.session.

ForceEagerSessionCreationFilter

200

org.springframework.security.web.access.channel.

ChannelProcessingFilter

300

org.springframework.security.web.context.request.async.

WebAsyncManagerIntegrationFilter

500

org.springframework.security.web.context.

SecurityContextHolderFilter

600

org.springframework.security.web.context.

SecurityContextPersistenceFilter

700

org.springframework.security.web.header.

HeaderWriterFilter

800

org.springframework.web.filter.

CorsFilter

900

org.springframework.security.web.csrf.

CsrfFilter

1000

org.springframework.security.web.authentication.logout.

LogoutFilter

1100

org.springframework.security.oauth2.client.web.

OAuth2AuthorizationRequestRedirectFilter

1200

org.springframework.security.saml2.provider.service.servlet.filter.

Saml2WebSsoAuthenticationRequestFilter

1300

org.springframework.security.web.authentication.preauth.x509.

X509AuthenticationFilter

1400

org.springframework.security.web.authentication.preauth.

AbstractPreAuthenticatedProcessingFilter

1500

org.springframework.security.cas.web.

CasAuthenticationFilter

1600

org.springframework.security.oauth2.client.web.

OAuth2LoginAuthenticationFilter

1700

org.springframework.security.saml2.provider.service.servlet.filter.

Saml2WebSsoAuthenticationFilter

1800

org.springframework.security.web.authentication.

UsernamePasswordAuthenticationFilter

1900

org.springframework.security.openid.

OpenIDAuthenticationFilter

2100

org.springframework.security.web.authentication.ui.

DefaultLoginPageGeneratingFilter

2200

org.springframework.security.web.authentication.ui.

DefaultLogoutPageGeneratingFilter

2300

org.springframework.security.web.session.

ConcurrentSessionFilter

2400

org.springframework.security.web.authentication.www.

DigestAuthenticationFilter

2500

org.springframework.security.oauth2.server.resource.web.

BearerTokenAuthenticationFilter

2600

org.springframework.security.web.authentication.www.

BasicAuthenticationFilter

2700

org.springframework.security.web.savedrequest.

RequestCacheAwareFilter

2800

org.springframework.security.web.servletapi.

SecurityContextHolderAwareRequestFilter

2900

org.springframework.security.web.jaasapi.

JaasApiIntegrationFilter

3000

org.springframework.security.web.authentication.rememberme.

RememberMeAuthenticationFilter

3100

org.springframework.security.web.authentication.

AnonymousAuthenticationFilter

3200

org.springframework.security.oauth2.client.web.

OAuth2AuthorizationCodeGrantFilter

3300

org.springframework.security.web.session.

SessionManagementFilter

3400

org.springframework.security.web.access.

ExceptionTranslationFilter

3500

org.springframework.security.web.access.intercept.

FilterSecurityInterceptor

3600

org.springframework.security.web.access.intercept.

AuthorizationFilter

3700

org.springframework.security.web.authentication.switchuser.

SwitchUserFilter

3800

Подробнее узнать про порядок можно заглянув с отладкой внутрь экземпляра HttpSecurity, в его переменную типа FilterOrderRegistration, это происходит прямо в её конструкторе.

Контроллер

Теперь добавим простенький контроллер

Код контроллера
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Controller {

    @GetMapping
    public String get() {
        return "Hello!";
    }
}

Тесты

...И проверим

Код теста
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
public class ControllerIT {

    @Test
    public void test() throws IOException, InterruptedException {
        final HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8080"))
                .GET()
                .build();
        final HttpClient client = HttpClient.newHttpClient();

        final HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        assertEquals("Hello!", response.body());
    }
}

Всё работает.


Кстати, для самостоятельного ознакомления, можно воспользоваться советом из документации Спринга:

...adding a debug point in FilterChainProxy is a great place to start.

В цепи обработки вы увидете "springSecurityFilterChain" (помните этот боб?), его собственно мы и настраиваем. Да, фильтры в фильтрах, к этом можно привыкнуть. Не сразу, но можно.

/* FilterChainProxy#doFilter(ServletRequest request, ServletResponse response, FilterChain chain)):

переменная chain#filters содержит 
0 = {ApplicationFilterConfig@7248} "ApplicationFilterConfig[name=characterEncodingFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter]"
1 = {ApplicationFilterConfig@7249} "ApplicationFilterConfig[name=formContentFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedFormContentFilter]"
2 = {ApplicationFilterConfig@7250} "ApplicationFilterConfig[name=requestContextFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter]"
3 = {ApplicationFilterConfig@7251} "ApplicationFilterConfig[name=springSecurityFilterChain, filterClass=org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$1]"
4 = {ApplicationFilterConfig@7252} "ApplicationFilterConfig[name=Tomcat WebSocket (JSR356) Filter, filterClass=org.apache.tomcat.websocket.server.WsFilter]"
*/

Tags:
Hubs:
Total votes 6: ↑1 and ↓5-4
Comments3

Articles