안녕하세요.
이번 글에서는 Spring Security가 어떻게 Filter로 등록되고 동작하는지에 대해서 정리하고자 합니다.
'필터와 인터셉터란?' 포스팅의 연장선에 있는 글이므로 아래를 참고하시면 이해하는데 도움이 될 수 있습니다.
https://okkkk-aanng.tistory.com/35
필터와 인터셉터란?
안녕하세요.이번 글에서는 필터와 인터셉터에 대한 내용을 다루고자 합니다.'필터와 인터셉터를 내가 과연 제대로 알고 있을까?'와 같은 반성을 하게 된 개인적인 사건(?)이 있었고, 이에 대한
okkkk-aanng.tistory.com
DelegatingFilterProxy와 SecurityFilterChain
DelegatingFilterProxy를 통해 서블릿 컨테이너는 스프링 컨텍스트에 Bean으로 등록된 Filter를 호출할 수 있습니다. 스프링 시큐리티도 DelegatingFilterProxy를 활용하여 구현되어 있습니다. 웹 어플리케이션이 실행될 때, 아래와 같이 DelegatingFilterProxy를 등록합니다.
public abstract class AbstractSecurityWebApplicationInitializer implements WebApplicationInitializer {
private void insertSpringSecurityFilterChain(ServletContext servletContext) {
String filterName = "springSecurityFilterChain";
DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy(filterName);
String contextAttribute = this.getWebApplicationContextAttribute();
if (contextAttribute != null) {
springSecurityFilterChain.setContextAttribute(contextAttribute);
}
this.registerFilter(servletContext, true, filterName, springSecurityFilterChain);
}
}
DelegatingFilterProxy를 통해 springSecurityFilterChain이라고 등록된 Bean에게 보안 관련 처리를 위임하고 있었습니다.
그렇다면 springSecurityFilterChain을 이름으로 갖는 Bean은 어디서 등록되는지를 확인했고, 이는 WebSecurityConfiguration 설정 클래스에 존재하는 것을 확인했습니다.
@Configuration(
proxyBeanMethods = false
)
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
private WebSecurity webSecurity;
private List<SecurityFilterChain> securityFilterChains = Collections.emptyList();
@Bean(
name = {"springSecurityFilterChain"}
)
public Filter springSecurityFilterChain() throws Exception {
// ...
Iterator var7 = this.securityFilterChains.iterator();
while(true) {
while(var7.hasNext()) {
SecurityFilterChain securityFilterChain = (SecurityFilterChain)var7.next();
this.webSecurity.addSecurityFilterChainBuilder(() -> {
return securityFilterChain;
});
Iterator var5 = securityFilterChain.getFilters().iterator();
while(var5.hasNext()) {
Filter filter = (Filter)var5.next();
if (filter instanceof FilterSecurityInterceptor) {
this.webSecurity.securityInterceptor((FilterSecurityInterceptor)filter);
break;
}
}
}
var7 = this.webSecurityCustomizers.iterator();
while(var7.hasNext()) {
WebSecurityCustomizer customizer = (WebSecurityCustomizer)var7.next();
customizer.customize(this.webSecurity);
}
return (Filter)this.webSecurity.build();
}
}
}
WebSecurityConfiguration 설정 클래스는 springSecurityFilterChain을 빈으로 등록하고 있으므로, 스프링 시큐리티를 사용하는 어플리케이션은 위 설정 클래스를 @Import 하여 커스텀 환경설정 구성이 필요합니다. 이를 위해 사용되는 것이 @EnableWebSecurity 어노테이션 입니다. 예를 들면 아래와 같습니다.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class, HttpSecurityConfiguration.class})
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
boolean debug() default false;
}
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// ...
return http.build();
}
}
즉, 어플리케이션에 맞는 커스텀 스프링 시큐리티 설정 클래스는 @EnableWebSecurity 어노테이션을 통해 springSecurityFilterChain을 Bean으로 등록할 수 있었고, 서블릿 컨테이너를 DelegatingFilterProxy를 통해 보안 관련 필터로 위 체인을 호출할 수 있는 구조였습니다.
Spring Security 6
Spring Boot 3.0 이상의 버전에서 지원하는 Spring Security 6 버전 부터는 DelegatingFilterProxy를 사용하지 않고, FilterProxyChain을 직접 서블릿 필터에 등록하여 보안 필터를 구성합니다. 참고한 소스는 아래와 같습니다.
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration {
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain(List<SecurityFilterChain> securityFilterChains) {
return new FilterChainProxy(securityFilterChains); // SecurityFilterChain들을 FilterChainProxy로 래핑
}
}
@Component
public class SecurityInitializer implements ServletContextInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
Filter springSecurityFilterChain = ...; // FilterChainProxy
servletContext.addFilter("springSecurityFilterChain", springSecurityFilterChain)
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
}
}
서블릿 컨테이너에서 스프링 컨텍스트에 저장된 Bean을 필터로 사용하기 위해서는 DelegatingFilterProxy가 필요했습니다. 왜냐하면 서블릿 컨테이너에서 직접 스프링 컨텍스트의 Bean에 접근할 수 없었기 때문입니다. 그러나, 스프링 부트의 최신 버전부터는 서블릿에서도 스프링 컨텍스트를 관리할 수 있게 되었고, 이에 따라 DelegatingFilterProxy는 굳이 사용하지 않고 서블릿 컨테이너에 필터를 직접 등록하여 사용할 수 있게 되었습니다. 이러한 설계 사상에 맞추어 Spring Security 6에서는 DelegatingFilterProxy를 제거하였다는 것을 파악할 수 있었습니다.
감사합니다.
'기술 블로그' 카테고리의 다른 글
@Transactional Deep Dive (1) | 2025.02.19 |
---|---|
Java Annotation (2) | 2025.02.18 |
Java ObjectMapper (Feat. RedisTemplate) (2) | 2025.02.13 |
스프링 Kafka Consumer Deep Dive - 1 (1) | 2025.02.05 |
필터와 인터셉터란? (0) | 2024.12.11 |