Spring Security 아키텍처에 대해 설명하기 전에 Filter가 무엇인지 알아보자!!
filter는 사용자의 요청이 Servlet에 도달하기 전/후 특정 로직(인증,예외처리 등등..)을 처리할 수 있도록 하는 J2EE 표준 스펙 기능이다. J2EE 표준 스펙임으로 filter는 Spring 범위(Spring Container) 밖의 Servlet Container에서 기능한다. 글의 주제에서 벗어나지만 여담으로 WAS는 Web Server+Web Container(Servlet Container)로 구성되어 있다.
위 그림처럼 사용자 요청이 Servlet 전/후 도달할 때 Filter 처리가 가능하다. 여러개의 Filter 사용하여 하나의 요청에 여러 Filter가 적용되도록 할 수도 있다. 여러 개의 Filter와 Servlet을 묶어 FilterChain이라고 부른다. 이때 Spring MVC apllication인 경우, Servelt은 DispatcherServlet을 의미한다. Filter의 힘은 Filter가 삽입된 Filter Chain에 의해서 나오고 삽입된 Filter들의 순서는 매우 중요하다.
스프링은 Filter의 구현체인 DelegatingFilterProxy를 제공한다. Servlet Container는 Servlet 표준 기술을 사용하여 Servlet Filter 인스턴스들을 등록할 수 있지만 스프링에서 정의된 빈들을 인식할 수 없어 빈을 주입 받을 수 없다. 이에 대한 해결책이 DelegatingFilterProxy인데 표준 Servlet Container 기술을 이용하여 DelegatingFilterProxy를 등록하고 Filter를 구현한 스프링 Bean에게 모든 작업을 위임할 수 있다. DelegatingFilterProxy는 ApplicationContext에서 빈으로 등록된 Filter를 찾고 호출한다. 위 그림에서 Bean Filter0가 그것이다.
설명만 들어서는 이해가 잘 안 될수도 있으니 코드를 뜯어보자!
위 코드는 DelegatingFilterProxy 클래스의 doFilter 메소드이다. 해당 클래스는 delegate이라는 Filter 타입의 변수를 가지고 있는데 만약 위임해줄 Filter(ex.Bean Filter0)가 존재하지 않는다면(delegateToUse == null) findWebApplicationContext()와 initDelegate()를 통해 ApplicationContext에서 빈으로 등록된 Filter를 찾아 클래스 변수(delegate)에 초기화한다. 이후 invokeDelegate()함수를 통해 주입 받았던 빈으로 등록된 Filter를 호출한다.
위 코드는 DelegatingFilterProxy 클래스에 initDelegate()메소드이다. 해당 메소드를 통해 ApplicationContext에서 targetBeanName에 해당하는 빈을 찾아 주입 받는다.
FilterChainproxy는 Spring Security에서 제공해주는 특별한 Filter 구현체이다. FilterChainproxy 역시 Filter 인스턴스에게 작업을 위임하는데 SecurityFilterChain을 통해 많은 Filter 인스턴스에게 작업 위임이 가능하고 여러 SecurityFilterChain 중 하나를 선택할 수 있다. FilterChainProxy는 등록된 빈임으로 DelegatingFilterProxy에 감싸져있다. 여기서 깜싸져 있다는 의미는 위 코드에서 확인 했듯이 DelegatingFilterProxy 클래스의 Filter 타입의 변수가 FilterChainProxy이라는 의미이다.
각각의 SecurityFilterChain에는 여러 개의 Filter 구현체들이 존재한다. 각각의 구현체를 우리는 앞으로 그냥 Filter가 아닌 Security Filter라고 부르겠다. 그 이유는 Servlet Filter와 구분하기 위함이다.
FilterChainProxy는 여러 SecurityFilterChain을
List<SecurityFilterChain> filterChains;
형태로 가지고 있다. 그리고 가지고 있는 여러 SecurityFilterChain 중에 요청 URL에 가장 먼저 matching되는 SecurityFilterChain 하나를 선택하여 작업을 위임하고 여러 Security Filter들을 통해 보안 처리(인증/인가)가 가능하다. Security Filter들은 대개 Bean으로 등록되어 있고 개발자가 필요한 Filter는 직접 SecurityFilterChain의 원하는 위치에 삽입이 가능하다.
아래 코드는 Custom Filter를 SecurityFilterChain에 삽입하는 예시 코드이다.
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtAuthenticationProvider jwtAuthenticationProvider;
public JwtAuthenticationFilter(JwtAuthenticationProvider jwtAuthenticationProvider) {
this.jwtAuthenticationProvider = jwtAuthenticationProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException, JwtException {
// ...
filterChain.doFilter(request, response);
}
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterBefore(new JwtAuthenticationFilter(jwtAuthenticationProvider),
UsernamePasswordAuthenticationFilter.class);
// ...
return http.build();
}
이때 주의할 점이 존재하는데 Spring Security 공식 문서에 의하면,,
Be careful when you declare your filter as a Spring bean, either by annotating it with @Component or by declaring it as a bean in your configuration, because Spring Boot will automatically register it with the embedded container. That may cause the filter to be invoked twice, once by the container and once by Spring Security and in a different order.
라고 나와있다. 쉽게 설명하면 @Component 등을 이용하여 Filter 인터페이스 구현한 Custom Filter를 빈으로 등록해 버리면 Servlet Filter Chain과 Spring Security Filter Chain에 이중 등록이 된다. 따라서
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("URL");
}
를 통해 Spring Security Filter를 적용하지 않는 URL에 해당하는 요청도 직접 삽입한 Custom Spring Security Filter(ex. JwtAuthenticationFilter)에 영향을 받는다!!
설명만 들어서는 이해가 잘 안 될수도 있으니 코드를 뜯어보자!
FilterChainProxy의 doFilter()메소드는 doFilterInternal()메소드를 호출한다.
List< Filter> filters = getFilters(firewallRequest);를 통해서 해당 SpringSecurityChain에 삽입된 Filter들을 불러온다.
getFilters()함수를 보면 요청온 URL에 matching되는 SpringSecurityChain을 찾고 해당 Chain에 존재하는 Filter들을 반환하는 것을 확인할 수 있다. getFilters()를 통해 불러온 Secuirty filters들을 doFilterInternal()함수 내부의 decorate().doFilter()를 통해 실행한다.