spring

Spring / Spring Security filter chain 들여다보기

e4g3r 2025. 3. 29. 02:00

Spring 혹은 Spring Security를 사용하면 filter라는 키워드를 종종 듣게 됩니다.


단순히 Http 요청이 Spring Application 영역으로 전달되기 전에 Servlet단에서 chain 형식으로 연결되어 있는 filter들을 하나씩

실행함으로써 Http 요청 응답을 가공할 수 있고 Spring Security Filter를 통해 인가/인증 처리를 한다는 정도만 알고 있었습니다.

종종 이러한 특성을 이용해 Http 요청 및 응답을 Filter 단에서 Logging을 하기도 합니다.

 

이정도만 알면 되겠지 했는데 최근 참여중인 오픈채팅방에서 Spring Security 관련하여 어떤분이 질문을 하셔서

저도 돌이켜보는 과정에서 너무 얕게 사용법만 알고 있는 것 같아 Filter Chain의 처리 과정을 디버깅을 통해 한번 들여다 보았습니다.

Servlet Filter에서 어떻게 Spring Bean을?

 

https://mossgreen.github.io/Servlet-Containers-and-Spring-Framework/

 

Servlet Container는 사용자로부터 Http 요청을 받아 적절한 형태로 가공한 후 Spring Container에게 전달을 합니다.
Spring Container는 Servlet으로부터 받은 요청을 처리할 수 있는 handler(controller)를 찾아 처리하도록 합니다.

Http 프로토콜을 처리하는 것은 Servlet Container, 비즈니스 로직을 통해 요청을 처리하는 것은 Spring Container라고 볼 수 있습니다.

 

Spring을 사용하면서 들었던 filter는 일반적으로 Servlet filter를 의미하는 것이었습니다.

그리고 보통 filter하면 interceptor가 같이 언급이 됩니다. 그리고 이 둘의 차이를 물어보는 게 유명한데요.

 

filter는 Http 요청 전/후로 Servlet 컨테이너 영역에서 처리되는 것이며
interceptor의 경우 dispatcherServlet이 handler에게 요청을 위임하기 전/후에 Spring 영역에서 처리되는 것이라고 말합니다.

 

Spring Security를 사용하면 일반적으로 SecurityFilterChain을 정의해서 사용합니다.

여기서 Filter 또한 Servlet Filter와 관련이 있기 때문에 처리 과정을 디버깅을 통해 들여다보았습니다.

 

디버깅을 통해 확인해보면 Spring Security Filter Chain은 GenericFilterBean을 상속하는 FilterChainProxy 클래스로 존재합니다.

 

그런데 Spring Security를 포함하여 일반적으로 사용했던 filter들은 Spring Bean을 통해 관리되는 객체입니다.

 

예를 들어 JWT 토큰을 통해 인증을 처리하는 JwtAuthentiactionFilter는 @Component를 통해 Spring Bean으로 등록했고
의존성 주입을 통해 tokenManager, userRepository를 주입받고 있습니다.

 

filter는 spring container 영역이 아닌 servlet 영역이라고 했던 것을 다시 생각해보면

servlet에서 어떻게 spring bean의 존재를 알고 있는 것이지? 라는 의문이 들 수 있습니다.

(예전에 이런 의문이 들긴 했는데 Spring Boot는 무언가 방법이 있었을 거야 하고 넘어갔음)

 

우연히 Spring Filter Chain을 디버깅 하는 과정에서 이 의문을 해결하게 되었습니다.

DelegatingFilterProxy

 

https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-filters-review

 

말했듯이 흔히 말하는 Filter는 Servlet Container 영역에서 chain 형태로 연결되어 있으며 순차적으로 하나씩 처리되는 구조입니다.

Spring boot의 내장 Servlet 서버 Tomcat의 경우는 Filter의 시작점은 ApplicationFilterChain 입니다.

 

ApplicationFilterChain에서는 등록 된 Servlet Filter들을 배열 형태로 관리하고 있음을 볼 수 있었습니다.

인덱스 4에는 Spring Security와 관련 된 Spring Security Filter Chain이 등록되어 있음을 볼 수 있습니다.

 

ApplicationFilterChain의 internalDoFilter 메서드에서 등록 된 필터들을 하나 씩 순회하며 실행하는 것을 확인할 수 있습니다.

 

Spring Security Filter의 차례가 되었고 어떻게 처리되는지 알아보기 위해 filter를 타고 들어갔으나

Spring Security Filter Chain이 아닌 DelegatingFilterProxy 클래스를 만나게 되었습니다.


DelegatingFilterProxy는 내부적으로 delegate 필드에 실행하고자 하는 실제 Filter(Spring Security Filter)가 담겨있었습니다.

 

내부에 doFilter 메서드를 통해 실제 Filter를 호출합니다. 클래스의 이름처럼 Proxy 형태라고 볼 수 있습니다.

표준 서블릿 필터의 프록시로, 필터 인터페이스를 구현하는 Spring 관리 빈에 위임합니다. 
일반적으로 지정된 필터 이름이 Spring의 루트 애플리케이션 컨텍스트에 있는 빈 이름에 해당하는 DelegatingFilterProxy
정의가 포함됩니다. 그러면 필터 프록시에 대한 모든 호출이 Spring 컨텍스트에서 해당 빈으로 위임되며,
이는 표준 서블릿 필터 인터페이스를 구현하는 데 필요합니다.
공식문서

 

공식문서를 나름 해석해보면 DelegatingFilterProxy가 대신 Spring Container에 존재하는 Filter를 호출해주는 것이라고 합니다.

Servlet 영역에서 Spring 영역에 있는 필터 객체를 사용할 수 있게 해주는 중간 다리 역할이라고 볼 수 있습니다.

 

Filter를 DelegatingFilterProxy 형태로 만들어서 Servlet Filter에 등록해주면 된다고 합니다.

 

Spring Security는 Servlet Filter에 Spring Security Filter Chain을 추가하기 위해 내부적으로 DelegatingFilterProxy

형태로 감싸는 것으로 보입니다.

 

이처럼 Proxy 형태로 감싸서 Servlet 영역에서 스프링이 관리하는 Filter를 사용할 수 있도록 하는 것이었습니다.

Spring Boot면 사실 상관 없음

그런데 Spring boot 환경에서는 DelegatingFilterProxy가 꼭 필요한 것은 아닙니다.

Spring Boot의 경우 Tomcat(Servlet)을 제어할 수 있는 환경이기 때문입니다.

 

실제 Filter 등록 과정을 확인하기 위해 임시로 Filter를 만들어서 Bean으로 등록해보았습니다.

AbstractFilterRegistrationBean

디버깅을 통해 확인해본 결과 Spring Boot는 AbstractFilterRegistrationBean 클래스를 통해 Servlet에 Filter를 추가합니다.

 

addRegistration 메서드를 통해 직접 servlectContext에 addFilter를 해버립니다.

따라서 Spring Boot 환경에서는 Filter를 DelegatingFilterProxy로 감싼 다음 등록하지 않아도 됩니다.

 

만약 Spring Boot가 아닌 Spring이라면 DelegatingFilterProxy 형태로 Filter를 만들고 직접 추가해줘야 합니다...

Spring Security Filter Chain

이어서 Spring Security Filter Chain이 어떻게 흘러가는지 살펴보았습니다.

 

https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-filters-review

 

앞서 디버깅 과정을 통해 확인한 것 처럼 Servlet Filter에 Spring Security Filter Chain이 추가됩니다.

추가 된 Spring Security Filter Chain 또한 여러 개의 Security Filter가 연결되어 있는 형태입니다.

또한 1개 이상의 Spring Security Filter Chain은 Filter Chain Proxy로 감싸져 있습니다.

그리고 Spring 영역의 FilterChainProxy를 Servlet Filter에서 사용가능하도록 DelegatingFilterProxy로 감싸고 있습니다.

 

즉 ServletFilterChain -> DelegatingFilterProxy -> FilterChainProxy -> SecurityFilterChain 순서로 진행됩니다.

 

DelegatingFilterProxy 내부에서흐름을 따라가다보면 FilterChainProxy 클래스를 만나게 됩니다.

 

위에서 언급 된 것 처럼 FilterChainProxy 내부 filterChains 필드에는 1개 이상의 Security Filter Chain이 존재하게 됩니다.

 

제 사이드 프로젝트에는 jwtAuthencationFilter를 추가한 Security Filter Chain 1개만 등록했기 때문에

FilterChainProxy 역시 1개의 filterChain만 가지고 있습니다.


여러 개의 Filter Chain이 있다면 어떤 Security Filter Chain이 선택되는지는 getFilters 메서드가 결정합니다.

 

getFilters 메서드는 존재하는 filterChain을 순회하면서 matches 메서드를 통해 대상 Filter인지 확인 후 사용하도록 하는데요.

 

위와 같이 2개의 SecurityFilterChain이 존재하고 2개 모두 /api로 시작하는 요청에 적용된다고 하면

두 개의 SecurityFilterChain 모두가 동작하는 것이 아닌 먼저 등록 된 SecurityFilterChain이 동작하게 됩니다.

 

따라서 겹치지 않도록 URL 패턴을 적용하거나 @Order를 통해 등록 순서를 정하는 방법이 있습니다.

 

위 코드처럼 filterChain1이 더 높은 우선 순위를 가진 경우

 

jwt 필터가 사용되는(6번 인덱스) filterChain1이 먼저 등록되었습니다.

 

반대로 filterChain2가 더 높은 우선 순위를 가진 경우

 

niceSpringBootFilter(6번 인덱스)가 사용되는 filterChain2가 먼저 등록되었음을 확인할 수 있었습니다.

 

이처럼 여러 개의 Security Filter Chain이 존재하며 URL 패턴이 겹칠 수 있는 경우 먼저 등록 된 필터 1개만 실행되므로

우선순위를 고려해야 합니다.

 

https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-multi-securityfilterchain-figure

 

여러 개의 SecurityFilterChain을 통해 URL 마다 다른 인증 / 인가 로직을 처리할 수 있으니 알아두면 좋을 것 같습니다.