안녕하세요. INCHEOL'S 입니다.
저번 시간에는 DelegatingFilterProxy를 소개해 드렸는데요. 오늘은 DelegatingFilterProxy에서 실제 작업을 위임받아 처리하는 FilterChainProxy를 소개드려볼까 합니다.
Spring Security를 사용하면 반드시 알아야 할 클래스로 디버깅이 필요하다면 저는 자주 이 클래스 내부에 먼저 break point를 찍고 시작합니다. 그만큼 중요한 역할을 담당하고 있으니 관심을 갖고 봐주세요:)
Dependency
Spring Boot 2.4.1<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
내용
1. FilterChainProxy
2. SecurityConfig로 SecurityFilterChain 만들기
먼저 FilterChainProxy가 무엇이고 어떤 역할을 하는지 살펴보겠습니다.
Spring Security 공식 레퍼런스에서는 이렇게 소개하고 있는데요.
Spring Security’s Servlet support is contained within FilterChainProxy. FilterChainProxy is a special Filter provided by Spring Security that allows delegating to many Filter instances through SecurityFilterChain. Since FilterChainProxy is a Bean, it is typically wrapped in a DelegatingFilterProxy.
Spring Security의 Servlet 지원은 FilterChainProxy에 포함되어 있습니다. FilterChainProxy는 Spring Security에서 제공하는 특수 필터로 SecurityFilterChain을 통해 많은 Filter 인스턴스에 위임 할 수 있습니다. FilterChainProxy는 Bean이므로 일반적으로 DelegatingFilterProxy로 래핑됩니다.
레퍼런스의 설명을 참고하면 FilterChainProxy 역시 처리를 위임하기 위한 SecurityFilterChain을 들고 있다고 하는데요. 단순히 하나의 SecurityFilterChain은 아닙니다.
SecurityFilterChain을 담도록 선언되 있는 필드도 List<>이며 각각의 다른 SecurityFilter들이 들어있는 SecurityFilterChain들이 담길 수 있습니다.
즉, 설정에 따라서 SecurityFilterChain이 하나고 될 수도 있고 여러개가 될 수도 있으며 SecurityFilterChain안에 걸릴 Security Filter들을 다르게 설정할 수 있습니다.
다만 한 요청의 URL 패턴에 따라 SecurityFilterChain 하나를 선택하여 해당 되는 Security Filter들을 타도록 됩니다. 아래 그림은 Spring 공식 레퍼런스에서 가져온 것이고 참고하여 봐주세요.
또한 FilterChainProxy는 고정된 bean name으로 "springSecurityFilterChain" 이며 Bean으로 등록됩니다. 이 부분은 WebSecurityConfiguration 클래스를 참고하여 보시면 되겠습니다.
그러면 위에서 봤던 그림과 동일하게 FilterChainProxy에 여러개의 SecurityFilterChain을 등록해보도록 하겠습니다. 우선 "/foo/**"를 처리하는 한개의 SecurityFilterChain을 만들어보고 원하는 대로 설정이 됐는지 확인하고 "/bar/**" 와 "/**" 를 각각 가진 SecurityFilterChain을 추가해보겠습니다.
먼저 SecurityConfig를 하나 만들어 /foo/** 의 요청을 받는 SecurityFilterChain을 만들어보겠습니다. SecurityConfig 클래스를 만든 후 아래와 같이 작성해 줍니다.
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.mvcMatcher("/foo/**");
}
}
WebSecurityConfigurerAdapter 클래스는 Spring Security에서 설정들을 다양하게 커스터마이징할 수 있는 변경 포인트를 제공하는 설정자 클래스입니다.(해당 클래스의 상속구조를 따라가다 보시면 SecurityConfigurer, WebSecurityConfigurer를 구현하는 것을 보실 수 있는데요, 이는 WebSecurityConfigurerAdapter를 상속받아 사용한다면 SecurityConfigurer 타입이되고 Spring Security에서는 해당 타입들을 참고하여 FilterChain 생성과 Security Filter 등의 작업을 하게됩니다.)
또한, Spring Security에서는 해당 설정자 타입들을 applicationContext에서 가져다 쓰기때문에 @Configuration 애너테이션을 붙여 Bean으로 등록하는 것이 필요합니다.
자, 이것으로 우리는 /foo/**를 처리하는 SecurityFilterChain을 만들었는데요. 정말 우리가 원하는대로 만들어졌는지 디버깅을 통해 확인해보겠습니다.
보시다시피, /foo/** 를 처리하는 SecurityFilterChain 만들어진 것을 확인할 수 있습니다.
그렇다면, /bar/** , /** 와 같은 SecurityFilterChain을 추가하고 싶다면 어떻게 해야될까요?
정답은 바로 WebSecurityConfigurerAdapter를 확장하는 설정 Config 클래스를 추가적으로 만드는 것 입니다. 왜냐하면 WebSecurityConfigurerAdapter를 확장하는 클래스 자체가 결국에는 하나의 SecurityFilterChain으로 등록이 되기 때문입니다.
그래서 아래와 같이 코드 변경을 하고 다시 디버깅을 통해 확인해봅시다.
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@Import({SecurityConfig.FooSecurityConfig.class, SecurityConfig.BarSecurityConfig.class, SecurityConfig.AllSecurityConfig.class})
public class SecurityConfig {
@Order(100)
static class FooSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.mvcMatcher("/foo/**");
}
}
@Order(200)
static class BarSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.mvcMatcher("/bar/**");
}
}
@Order(300)
static class AllSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.mvcMatcher("/**");
}
}
저는 위와 같이 SecurityConfig를 @Configuration 빈으로 만들고 안쪽에 static inner 클래스를 작성하여 구현하였습니다. 그리고 각각의 클래스들을 빈으로 등록하기 위해 @Import 애너테이션을 이용했구요.
다만, 여기서 주의하실 것은 제가 각각의 클래스에 @Order 애너테이션을 붙였는데요. @Order애너테이션이 없다면 애플리케이션 기동시에 모든 설정클래스의 Order가 100이기 때문에 Exception이 발생하여 기동에 실패합니다.
왜 Order를 정해야만 할까요? 그 이유는 SecurityFilterChain 들이 FilterChainProxy의 filterChains 리스트에 담기는 순서가 중요하기 때문입니다.
만약, AllSecurityConfig의 Order가 가장 낮다면 모든 요청은 해당 SecurityFilterChain에 걸리게 됩니다. 아무리 /foo/**, /bar/**를 보내도 원하는 FilterChain에 도달하지 않고 /** 에 먼저 걸리기 때문이죠.
그래서 Spring Security는 SecurityConfigurer에 대한 Order가 같은 것이 있다면 예외를 던지고 기동자체를 할 수 없게 만들어 유저가 이러한 부분들을 인식할 수 있게끔 해놓은 것 같습니다.(개인적인 생각입니다.)
자, 그러면 마지막으로 디버깅을 통하여 우리가 원하는대로 FilterChain들이 생성되었는지 확인해보도록 하겠습니다.
보시는바와 같이 FilterChain이 3개가 제가 Order에 지정된 값 순서대로 filterChains 리스트에 들어있음을 확인할 수 있습니다.
이것으로 오늘 FilterChainProxy에 대해 알아보았는데요.
기억해야 할 점은 FilterChain은 하나의 SecurityConfig로 만들어지고
여러개의 FilterChain을 만든다면 URL 패턴 정의와 그에 따른 순서 조정이 필요하다 입니다.