Статья - краткое напоминание о:
- какие фильтры по-умолчанию уже встроены, и как их убрать или донастроить,
- список прочих доступных уже готовых к использованию фильтров и полный их список с порядком,
- даже слишком краткий обзор методов 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]"
*/