Spring Security

[Spring Security 6.x] DelegatingFilterProxy / FilterChainProxy

person456 2024. 6. 12. 18:40

 

목차

  1. Filter
  2. DelegatingFilterProxy
  3. FilterChainProxy
  4. DelegatingFilterProxy와 FilterChainProxy의 동작

 


1. Filter

다른 내용들에 대해 설명하기 앞서 먼저 Filter에 대한 이해가 필요하다.

Filter는 WAS(Web Application Server, ex)Tomcat)에서 생성되고 실행되며 종료한다.

즉, Spring Contrainer와는 별개로 Servlet Container에서 동작한다는 의미이다.

따라서 Filter는 웹 어플리케이션에서 클라이언트의 요청과 서버의 응답을 가공하거나 검사하는데 사용된다.

 

위의 사진은 전형적인 Spring MVC(Spring Legacy)에서 사용되는 구조이다. Spring Legacy에서 Spring Container는

Sevlet Container 위에서 작동하기 때문에 Spring Container에 도달하기 이전 먼저 Servlet Container의 Filter를 거친다.

여기에서 생긴 궁금증은 "그렇다면 내장 Tomcat을 사용하는 Spring Boot의 경우는 어떨까?" 였다.

 

Spring Boot는 Spring Legacy와는 다르게 WAS를 Spring Container 내부에 두고있어 별도의 설정 없이 WAS를 동작시킬 수 있다.

만약 별도의 외부 WAS를 등록하고 사용한다면 위의 그림과 같은 동작을 하겠지만, 내장 Tomcat을 사용하는 경우가 궁금하여 찾아보았다.

 

Spring Boot에서도 마찬가지로 Servlet Container를 거친 이후 Spring Container를 접하는 것은 변함이 없었다.

하지만 그 두 영역이 모두 Spring Boot라는 영역 안에서 이루어 질 뿐, 해당 동작에 대한 변화는 없었다는 것이다.

 


 

2. DelegatingFilterProxy

 

DelegatingFilterProxy라는 이름을 해석해보자면 위임하는 역할의 FilterProxy라고 볼 수 있다.

DelegatingFilterProxy는 스프링에서 사용되는 특별한 Servlet Filter로, Servlet Container와 Spring Container를 연결해주는 연결고리 역할을 해주는 Filter이다.

기존 WAS와 Spring Container가 분리된 환경을 생각해보면 Filter가 동작하는 영역은 Spring Container가 아닌 WAS의 Servlet Container다.

따라서 Spring의 DI, AOP 등의 기능을 Filter에서 활용하는 것은 불가능하다. 따라서 이러한 문제를 해결하고자

DelegatingFilterProxy는  Servlet Filter의 기능도 수행하며 Spring 의존성 주입 및 Bean 관리 기능을 연동하고자  만들어진 Filter라고 볼 수 있다.

 

이름에서부터 Delegate가 들어가 예측이 가능하지만 해당 Filter는 별도의 Filtering 기능을 수행하지 않는다.

다만 "springSecurityFilterChain"이라는 이름으로 생성된 Spring Bean을 찾아 사용자의 요청을 해당 Bean에게 넘겨주는, 위임하는 역할을 진행한다.

 

해당 내용은 Spring 공식 문서에서 DelegatingFilterProxy에 관한 내용이다.

요약하자면

" 서블릿 컨테이너는 자체적으로 Filter를 등록하여 사용할 수 있지만 Spring에서의 Bean을 인식하지 못한다.

따라서 DelegatingFilterProxy를 통해 Filter를 구현하는 Spring Bean에게 해당 요청을 위임할 수 있다."

라는 내용이다.

 

해당 사진은 Servlet Container의 영역이며 Container 내부에 DelegatingFilterProxy를 등록하고, 해당 Proxy 객체는 Spring 내부에서의 Bean으로 등록된 Filter를 가리키고있다.

해당 Bean이 바로 "springSecurityFilterChain"이라는 이름으로 등록된 Bean이다.

해당 필터체인이 바로 HttpSecurity와 WebSecurity의 초기화 과정을 통해 만들어진 FilterChainProxy 객체다.

 


 

3. FilterChainProxy

 

FilterChainProxy는 WebSecurity에 의해 생성되는 Bean이며 동시에 "springSecurityFilterChain"이라는 이름으로 생성된다.

따라서 DelegatingFilterProxy에 의해 요청을 위임받고 필터링 기능을 수행하는 객체라 볼 수 있다.

해당 FilterChainProxy는 내부적으로 하나, 또는 그 이상의 SecurityFilterChain을 갖고 있으며 사용자의 Request 정보를 토대로 적절한 SecurityFilterChain과 내부의 Filter들을 이용해 Filtering 기능을 수행한다.

 

즉, DelegatingFilterProxy를 거치지 않고 FilterChainProxy에 도달할 방법은 없으며 이는 다시 말해

모든 요청은 DelegatingFilterProxy와 FilterChainProxy를 거쳐간다는 의미다.

 

 

FilterChainProxy는 SecurityFilterChain 타입의 List를 멤버로 갖고있다.

해당 SecurityFilterChain이 FilterChainProxy에 등록되는 과정은 다음과 같다.

  1. HttpSecurity에 의해 하나, 또는 여러 개의 SecurityFilterChain이 Bean으로 등록된다.
  2. WebSecurity에 의해 기존에 생성된 SecurityFilterChain이 SecurityBuilder에 저장된다.
  3. FilterChainProxy는 SecurityBuilder의 SecurityFilterChain을 매개변수로 하여 생성된다.

FilterChainProxy의 "filterChains" 멤버
FilterChainProxy의 생성자. 1개의 FilterChain을 받는 생성자와 List를 받는 생성자가 존재한다.

 

 

 


4. DelegatingFilterProxy와 FilterChainProxy의 동작

 

1) DelegatingFilterProxy의 생성

 @Bean
    @ConditionalOnBean(
        name = {"springSecurityFilterChain"}
    )
    public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(SecurityProperties securityProperties) {
        DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean("springSecurityFilterChain", new ServletRegistrationBean[0]);
        registration.setOrder(securityProperties.getFilter().getOrder());
        registration.setDispatcherTypes(this.getDispatcherTypes(securityProperties));
        return registration;
    }

 

SecurityFilterAutoConfiguration 클래스의 내용을 보면 DelegatingFilterProxyRegistrationBean을 Bean으로 등록하고있다.

해당 Bean에서는 

아래와 같이 DelegatingFilterProxy를 생성하는데, 이 때 targetBeanName은 "springSecurityFilterChain"이다.

이후, 해당 FilterProxy는 AbstractFilterRegistraionBean 클래스에서

 

Spring Container가 아닌 Servlet Context에 Filter로 등록되며 생성이 마무리된다.

 

2) FilterChainProxy의 생성

 

FilterChainProxy는 단계적으로 HttpSecurity에서 생성된 SecurityFilterChain을 기반으로, WebSecurity에 의해 생성된다.

WebSecurity는 WebSecurityConfiguration 설정 클래스로 인해 생성되며 이때 WebSecurity의 performBuild()에 의해 생성된다.

 

처음 FilterChainProxy가 생성될때 매개변수로 받는 securityFilterChains는 ArrayList 형식이며 반복을 통해 HttpSecurity가 생성한 SecurityFilterChain을 담고있는 것을 확인할 수 있다.

 

 

3) 두 FilterProxy의 동작

 

// DelegatingFilterProxy의 Method

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Filter delegateToUse = this.delegate;
        if (delegateToUse == null) {
            synchronized(this.delegateMonitor) {
                delegateToUse = this.delegate;
                if (delegateToUse == null) {
                    WebApplicationContext wac = this.findWebApplicationContext();
                    if (wac == null) {
                        throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
                    }

                    delegateToUse = this.initDelegate(wac);
                }

                this.delegate = delegateToUse;
            }
        }

        this.invokeDelegate(delegateToUse, request, response, filterChain);
    }

해당 메서드는 DelegatingFilterProxy의 doFilter Method다.

내용을 보면 만약 delegateToUse가 null이라면 WebApplicationContext에서부터 targetBeanName을 토대로 delegate를 init하는 것을 볼 수 있는데, targetBeanName은 앞서 언급한 "springSecurityFilterChain"이기 때문에 결과적으로는

Spring Container의 FilterChainProxy를 가리키는 것으로 초기화 하는 내용이다.

 

이후 this.invokeDelegate를 호출하는데 내용은 다음과 같다.

protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        delegate.doFilter(request, response, filterChain);
    }

 해당 내용은 DelegatingFilterProxy가 가리키는 FilterChainProxy의 doFilter를 호출하는것이다.

 

// FilterChainProxy의 doFilter Method

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
        if (!clearContext) {
            this.doFilterInternal(request, response, chain);
        } else {
            try {
                request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
                this.doFilterInternal(request, response, chain);
            } catch (Exception var11) {
                Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var11);
                Throwable requestRejectedException = this.throwableAnalyzer.getFirstThrowableOfType(RequestRejectedException.class, causeChain);
                if (!(requestRejectedException instanceof RequestRejectedException)) {
                    throw var11;
                }

                this.requestRejectedHandler.handle((HttpServletRequest)request, (HttpServletResponse)response, (RequestRejectedException)requestRejectedException);
            } finally {
                this.securityContextHolderStrategy.clearContext();
                request.removeAttribute(FILTER_APPLIED);
            }

        }
    }

위의 코드는 FilterChainProxy의 doFilter 메서드다.

내용을 보면 try - catch문의 doFilterInternal을 호출하는데, 해당 doFilterInternal은 FilterChainProxy에 내부적으로 존재하는 VirtualFilterChain 클래스의 doFilter를 호출한다.

VirtualFilterChain은 스프링 전용 필터이며 WAS에서 사용하는 Filter가 아닌 Spring에서 사용하는 가상의 필터를 만들어 이를 토대로 Filtering을 진행하는 것이다.

 

VirtualFilterChain의 doFilter

 

이후 갖고있는 Filter들을 모두 doFilter 하는것으로 DelegatingFilterProxy와 FilterChainProxy의 동작이 마무리된다.