Коротко, статья как напоминание мне, а может и вам, как быстро начать настраивать цепочки безопасности, что уже висит подключенным и как выключить все, что не нужно, быстро с нуля начать писать без обвесок, больше контроля и понимания.
Введение
Решая тестовое задание, когда доступ к ресурсу предоставляется не всем, я выбрал 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'
}
Настройка
Одним куском такую информацию так нигде и не встретил, пришлось поработать в ручную, пробами и ошибками. Итак, есть два предлагамых Спригом способа настройки безопасности через код (можно ещё через файл конфигурации): это через наследование классу WebSecurityConfigurerAdapter
(с версии 5.7 признан устаревшим), либо через класс конфигурации. На оба рекомендовано навесить аннотацию
'@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
- строитель того, что используют филтры, о которых идёт здесь речь).
Оказалось, что всё, что уже включено, это 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 chain = 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 |
Каждый фильтр внутри springSecurityFilterChain имеет свой порядок. Начальный задается внутри экземпляра 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.
В цепи обработки
/* 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]"
springSecurityFilterChain, его собственно мы и настраиваем
*/