티스토리 뷰

Spring Security는 필터를 기반으로 동작한다. 스프링 MVC에는 필터와 비슷한 개념인 인터셉터 라는 것이 존재하는데 이 둘의 차이점이 무엇인지 알아보자. 또한 스프링 MVC에서는 DispatcherServlet 이라는 것이 핵심적인 역할을 하는데, 이와 비슷하게 스프링 시큐리티에도 핵심 필터들이 존재한다. 해당 필터들은 무슨 역할을 하는지, 요청이 들어왔을 때 이를 처리하는 전체적인 플로우도 살펴보자.
 

Filter vs Interceptor

Java Servlet Filter vs Spring MVC HandlerInterceptor

Servlet Filter

먼저 필터는 자바의 Servlet Filter를 지칭하는 것으로 즉 스프링 프레임워크에 속한 기술이 아닌 웹서버 기술(서블릿)에 속한다. 따라서 클라이언트의 요청을 컨트롤러에서 처리하기 전 요청을 조작하거나 막을 수 있으며 컨트롤러에서 처리가 끝난 응답에 대한 조작 등도 가능하다. 필터 사용의 대표적인 예가 스프링 시큐리티이며 이러한 필터를 이용해 인증 및 인가를 처리하고 있다. (Spring Security는 Spring MVC 밖에서 사용된다.)

또한 필터는 Filter 인터페이스를 구현하며 doFilter 메소드에 해당 필터에서 원하는 동작을 정의한다. 추가적으로 필터는 필터 체인이라 불리는 연속적인 필터들로 관리되며 현재 필터에서 FilterChain의 doFilter 메소드를 호출함으로써 다음 필터에게 추가적인 처리를 위임할 수도 있다.
 
원래 필터는 스프링과는 무관한 기술로 아무런 설정을 하지 않으며 스프링은 필터를 인식할 수 없지만, @Component 어노테이션을 통해 스프링 컨테이너에 빈으로 등록할 수도 있다. 다음은 모든 요청 URL에 대해 로그를 남겨주는 역할을 하는 필터이다. 

@Component
public class LogFilter implements Filter {

    private Logger logger = LoggerFactory.getLogger(LogFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
      throws IOException, ServletException {
        logger.info("Hello from: " + request.getLocalAddr());
        chain.doFilter(request, response);
    }
}

 
 
이 외에도 @Bean 어노테이션과 FilterRegistrationBean 객체를 생성하여 필터를 빈으로 등록할 수 있다. 아래 필터의 경우 요청 URL이 /users/* 인 경우에 대해서만 적용이 되며, setOrder로 필터의 적용 순서를 설정할 수 있다. (순서값이 작을 수록 높은 우선순위)

@Bean
public FilterRegistrationBean<LogFilter> loggingFilter() {
    FilterRegistrationBean<LogFilter> registrationBean 
      = new FilterRegistrationBean<>();
        
    registrationBean.setFilter(new LogFilter());
    registrationBean.addUrlPatterns("/users/*");
    registrationBean.setOrder(1);
        
    return registrationBean;    
}

 
 
필터는 스프링 빈으로 등록하지 않고 서블릿 컨테이너에서만 관리되도록 할 수도 있다. @WebFilter은 Servlet 3.0 스펙에 새롭게 추가된 어노테이션으로, 아래 필터는 서블릿 컨테이너에 자동으로 등록된다. 추가로 @WebFilter 어노테이션이 선언된 필터를 인식하기 위해 @ServletComponentScan 어노테이션이 함께 선언되어야 한다. (해당 어노테이션이 선언된 패키지 및 하위 패키지를 스캔)

@WebFilter(urlPatterns = "/users/*")
public class LogFilter implements Filter {

    private Logger logger = LoggerFactory.getLogger(LogFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
      throws IOException, ServletException {
        logger.info("Hello from: " + request.getLocalAddr());
        chain.doFilter(request, response);
    }
}


 
 

HandlerInterceptor

인터셉터는 Spring MVC 프레임워크의 한 기술로 DispatcherServlet - Controllers 사이에 위치하고 있다. 따라서 요청이 컨트롤러에 도달하기 전 또는 컨트롤러 처리 후 뷰가 렌더링되기 전후에 요청과 응답을 가로챌 수 있다. 

public class LogInterceptor implements HandlerInterceptor {

    private Logger logger = LoggerFactory.getLogger(LogInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
      throws Exception {
        logger.info("preHandle");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) 
      throws Exception {
        logger.info("postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 
      throws Exception {
        logger.info("afterCompletion");
    }

}

인터셉터는 HandlerInterceptor 인터페이스를 구현해야 하며 재정의 가능한 메소드와 메소드 호출 시기는 다음과 같다.

- preHandle: 컨트롤러 호출 전
- postHandle: 컨트롤러 호출 후 뷰를 렌더링하기 전
- afterCompletion: 컨트롤러 호출 후 뷰를 렌더링한 후

또한 WebMvcConfigurer를 구현한 설정 클래스에서 addInterceptors 메소드를 재정의함으로써 인터셉터를 등록할 수 있다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new RequestInterceptor());
    }
}


 

주요 차이점

필터와 인터셉터는 거의 비슷하다고 볼 수 있지만 분명한 차이점이 존재한다. 우선 필터는 요청이 DispatcherServlet에 도달하기 전 인증, 로깅, auditing, 이미지 및 데이터 압축 등의 처리를 수행한다. (Spring MVC 프레임워크와 별개로 존재)

반면 인터셉터는 DispatcherServlet과 Controller 사이에 위치한다. 즉 Spring MVC 프레임워크 내에서 수행되기 때문에 필터와 달리 Handler 또는 ModelAndView 객체에 대한 접근이 가능하다. 따라서 인터셉터는 일반적으로 애플리케이션 로깅과 같은 cross-cutting 문제(AOP), 인가 등을 처리한다.


필터나 인터셉터나 클라이어트 요청에 대한 전처리가 가능하기 때문에 Spring Security 프레임워크에 대한 의존성 없이 필터나 인터셉터만으로도 인증 및 인가를 직접 구현할 수 있다. (스프링 시큐리티가 러닝 커브가 조금 있지만 구현 시에는 더 편리하다.)
 
 

Spring Security의 핵심 필터

Spring Security는 전반적으로 표준 Servlet Filter를 기반으로 하고 있다. 내부적으로 Filter Chain(a sequence of filters)을 유지하며 각각의 필터는 인증, 인가, csrf 공격 방어, 예외 처리 등 특정한 역할을 수행한다. 물론 이러한 필터들은 항상 고정되어 있는 것은 아니고, 설정에 따라 시큐리티 필터 체인에 추가되거나 제거되어 특정 필터가 동작하지 않게 할 수도 있다. (csrf 공격을 굳이 방어할 필요가 없다면 CsrfFilter를 사용하지 않도록 설정 가능) 또한 직접 구현한 커스텀 필터가 시큐리티 필터 체인의 특정 위치에 추가되도록 설정 가능하다.
 

스프링 시큐리티에서는 기본적으로 많은 필터를 제공하는데 주요 필터들과 이들의 역할에 대해 자세히 알아보기 전, Spring Security가 인증 및 인가가 필요한 요청에 대해 어떤 플로우로 이를 처리하는지 전체적인 구조를 살펴보고자 한다.


Spring Security에는 핵심적인 3가지 필터가 있다. 바로 DelegatingFilterProxy, FilterChainProxy, SecurityFilterChain 이다. 각 필터의 대략적인 역할은 다음과 같다.

- DelegatingFilterProxy: Servlet Filter의 일종으로 스프링 컨테이너에 등록된 필터 빈에게 요청 처리를 위임하는 역할을 한다. 즉 서블릿 컨테이너와 스프링 컨테이너 사이를 이어준다고 볼 수 있다.

- FilterChainProxy: 스프링 시큐리티가 자동으로 생성하는 스프링 빈으로, 빈 이름은 "springSecurityFilterChain"으로 고정되어 있다. 내부적으로 여러 SecurityFilterChain을 관리하고 있으며 FilterChainProxy 또한 요청에 따라 적절한 SecurityFIlterChain에게 처리를 위임한다.

- SecurityFilterChain: 스프링 빈으로 관리되는 시큐리티 필터들의 집합으로, 하나의 RequestMatcher와 여러 Filter들(필터 체인)을 관리하고 있으며, FilterChainProxy에서 요청 처리를 위임할 적절한 SecurityFIlterChain을 선정할 때 RequestMatcher를 사용한다. 
 
즉 실질적인 보안 처리는 SecurityFilterChain이 하며 DelegatingFilterProxy, FilterChainProxy는 이름(프록시) 답게 적절한 필터에게 요청 처리를 위임하는 역할을 수행한다. 그럼 이제 각 필터에 대해 자세히 알아보자.
 

DelegatingFilterProxy

일단 DelegatingFilterProxy는 Spring Security 프레임워크가 제공하는 필터는 아니다. 먼저 해당 필터 프록시가 등장하게 된 배경을 이야기 해보자. 원래 필터는 서블릿 기술로 스프링 빈으로 등록이 불가능했다. 필터 내에서 스프링 빈을 주입 받아야 할 필요성 등으로 인해 필터를 빈으로 관리하고자 하였고, 따라서 서블릿 컨테이너의 DelegatingFilterProxy가 스프링 컨테이너에 등록된 필터 빈에 요청 처리를 위임함으로써 기존의 필터 체인에 필터 빈을 정상적으로 포함할 수 있게 된 것이다. (참고로 Spring Boot에서는 내장 웹 서버를 지원하기 때문에 톰캣과 같은 서블릿 컨테이너까지 Spring Boot가 제어 가능하여 필터 빈을 관리하기 위해 더이상 DelegatingFilterProxy가 필요하지 않다.)


DelegatingFilterProxy는 Spring Application Context에 접근 가능한 Filter 구현 클래스(필터 빈)에 요청 처리를 위임하는 Servlet Filter 프록시로, Spring Security 프레임 워크는 이러한 DelegatingFilterProxy 기술을 도입하여 스프링 컨테이너 내에서 시큐리티 FilterChain이 관리되도록 구현되었다. 참고로 자바독에서는 DelegatingFilterProxy에 대해 다음과 같이 정의하고 있다. 

Proxy for a standard Servlet Filter, delegating to a Spring-managed bean that implements the Filter interface.

 

정리하자면 DelegatingFilterProxy는 서블릿 컨테이너에서 관리되지만 보안 처리를 하는 필터는 스프링 컨테이너에 빈으로 관리된다. 이때 DelegatingFilterProxy는 보안 필터들에게 요청 처리를 위임하기 때문에 Servlet Container와 Spring Application Context 사이를 이어주는 역할을 한다고 볼 수 있다. 그리고 스프링 시큐리티의 DelegatingFilterProxydoFilter 메소드 내에서 모든 클라이언트 요청에 대해 오직 하나의 필터 빈에게 처리를 위임하며 이때 하나의 필터 빈은 이어서 살펴볼 FilterChainProxy이다.  

 
여기서 의문이 들 수 있다. 왜 보안 필터는 스프링 빈으로 관리되어야 하는 것일까? 일반적으로 Filter를 구현한 클래스 객체는 서블릿 컨테이너에서 관리되며 이 경우 스프링은 해당 필터를 인식하지 못한다. 즉 스프링이 보안 필터를 인식하기 위해서이며, 또한 필터를 빈으로 관리함으로써 DI 뿐만 아니라 필터를 빈의 생성, 초기화, 소멸 등의 라이프사이클에 따라 관리할 수 있기 때문이다. 다시 한번 말하지만 DelegatingFilterProxy는 Spring Security 프레임워크가 제공하는 필터가 아니다. 시큐리티 필터를 스프링 빈으로 관리할 필요가 있었고 이를 위해 DelegatingFilterProxy를 도입하게 된 것이다. 현재 Spring Boot에서는 이를 사용하지 않으나 Spring Security는 해당 기술을 활용하여 필터 빈인 FitlerChainProxy에게 보안 처리를 위임하고 있는 것이다. 
 

public class DelegatingFilterProxy extends GenericFilterBean {

    private WebApplicationContext webApplicationContext;

    private String targetBeanName;
    
    private Filter delegate;
    
    //...
    
    @Override
    protected void initFilterBean() throws ServletException {
    	synchronized (this.delegateMonitor) {
            if (this.delegate == null) {
                if (this.targetBeanName == null) {
                    this.targetBeanName = getFilterName();
                }
                WebApplicationContext wac = findWebApplicationContext();
                if (wac != null) {
                    this.delegate = initDelegate(wac);
                }
            }
        }
    }
    
    protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        String targetBeanName = getTargetBeanName();
        Filter delegate = wac.getBean(targetBeanName, Filter.class); //처리를 위임할 필터 빈을 스프링 컨테이너에서 조회
        if (isTargetFilterLifecycle()) {
            delegate.init(getFilterConfig());
        }
        return delegate;
    }
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
        Filter delegateToUse = this.delegate;
        //...
        invokeDelegate(delegateToUse, request, response, filterChain); //필터 빈에 처리를 위임
    }
    
    protected void invokeDelegate(
        Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        delegate.doFilter(request, response, filterChain);
    }
}

DelegatingFilterProxy를 초기화하는 initFilterBean 메소드 내에서는 요청을 위임할 필터 빈도 초기화하고 있다. 이때 break point를 찍어 디버깅해보면 targetBeanName 값은 "springSecurityFilterChain"으로 스프링 빈으로 관리되는 FIlterChainProxy의 빈 이름이다. 그리고 모든 요청에 대해 doFilter 메소드를 실행할 때 invokeDelegate 메소드를 호출하여 FilterChainProxy에게 요청 처리를 위임한다. 


 

FilterChainProxy

FilterChainProxy는 특수한 Filter로서, Spring Security 프레임워크 내에서 빈으로 등록된 프록시 FilterChain을 의미한다. 이 필터 또한 요청을 실질적으로 처리하진 않는다. 내부적으로 여러 개의 SecurityFilterChain 이라는 것을 가지고 있는데 FilterChainProxy는 현재의 요청을 처리할 수 있는 적절한 SecurityFilterChain을 선택하여 처리를 위임한다. 
 

public class FilterChainProxy extends GenericFilterBean {

    private List<SecurityFilterChain> filterChains;
    
    private FilterChainDecorator filterChainDecorator = new VirtualFilterChainDecorator();
 
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
	        throws IOException, ServletException {
        //...
        doFilterInternal(request, response, chain);
        //...
    }

    private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        List<Filter> filters = getFilters(firewallRequest); //처리를 위임할 시큐리티 필터 체인 조회
        if (filters == null || filters.size() == 0) {
            //...
            this.filterChainDecorator.decorate(chain).doFilter(firewallRequest, firewallResponse);
            return;
        }
        FilterChain reset = (req, res) -> {
            chain.doFilter(req, res);
        };
        this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
    }
    
    private List<Filter> getFilters(HttpServletRequest request) {
        for (SecurityFilterChain chain : this.filterChains) {
            if (chain.matches(request)) { //현재 요청을 처리할 수 있는 SecurityFilterChain
                return chain.getFilters();
            }
        }
        return null;
    }
}

 

현재 요청에 대해 적절한 SecurityFilterChain을 선택하는 로직은 getFilters 메소드에 구현되어 있다. 즉 filterChains 리스트를 for문으로 순회하면서 해당 SecurityFilterChainmatches 메소드를 호출하여 현재 요청을 처리할 수 있는 필터 체인(List<Filter>)을 조회하여 반환한다. 그리고 해당 필터 체인에게 다시 요청 처리를 위임한다. 



SecurityFilterChain

SecurityFilterChain 또한 스프링 빈으로 관리되는 필터 체인으로 FilterChainProxy로부터 요청 처리를 위임받는다. 내부적으로 하나의 RequestMatcherFilter 리스트를 가지고 있어 RequestMatcher로는 자신이 처리할 수 있는 요청인지 여부를 판단하며(URL 기반), 처리 가능한 요청인 경우 Filter 리스트 즉 필터 체인이 본격적인 인증 및 인가 처리를 수행한다. 

public interface SecurityFilterChain {

    boolean matches(HttpServletRequest request);
    List<Filter> getFilters();
}

//SecurityFilterChain 인터페이스를 구현한 기본 SecurityFilterChain
public final class DefaultSecurityFilterChain implements SecurityFilterChain {

    private final RequestMatcher requestMatcher;
    private final List<Filter> filters;

    @Override
    public boolean matches(HttpServletRequest request) {
        return this.requestMatcher.matches(request);
    }
    
    @Override
    public List<Filter> getFilters() {
        return this.filters;
    }
}



 

최종적으로 다음과 같은 구조가 완성된다. FilterChainProxy는 필터 빈으로 DelegatingFitlerProxy로 감싸져 필터 체인에 등록되며,  모든 요청에 대해 DelegatingFilterProxy 필터가 일단 동작한다. 이후 DelegatingFilterProxy로부터 요청 처리를 위임받은 FilterChainProxy는 여러 개의 SecurityFilterChain 중 적절한 SecurityFilterChain을 조회하여 다시 요청 처리를 위임한다. (예를 들어 아래의 경우 현재 요청이 /api/** 라면 SecurityFilterChain0의 doFilter 메소드를 호출한다.)

 

Spring Security의 요청 처리 흐름

 


 

다중 보안 설정

FilterChainProxy 디버깅

기본적으로 FilterChainProxy 내에는 SecurityFilterChain이 하나만 등록된다. 그러나 다음과 같이 다중 보안 설정, 즉 여러 개의 SecurityFilterChain을 빈으로 등록하면 당연한 이야기이지만 FilterChainProxy는 여러 개의 SecurityFilterChain에 의존하게 되며, 이 중 적절한 시큐리티 필터 체인에게 요청 처리를 위임한다.

@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/", "/home") //"/", "/home" 요청을 처리할 수 있는 SecurityFilterChain 등록
            .authorizeHttpRequests(request -> request
                .requestMatchers("/", "/home").authenticated())
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll())
            .logout(LogoutConfigurer::permitAll);
        return http.build();
    }

    @Bean
    public SecurityFilterChain securityFilterChain2(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/admin") //"/admin" 요청을 처리할 수 있는 SecurityFilterChain 등록
            .authorizeHttpRequests(request -> request
                .requestMatchers("/admin").authenticated())
            .formLogin(AbstractHttpConfigurer::disable)
            .csrf(CsrfConfigurer::disable);
        return http.build();
    }
}



SecurityConfig 설정 시 securityMatcher api를 통해 특정 URL에 대해 동작할 SecurityFilterChain을 지정할 수 있으며, 실제 FilterChainProxy getFilters 메소드에 중단점을 찍어 디버깅해보면 위 설정을 통해 총 2개의 SecurityFilterChain에 의존하고 있는 것을 확인할 수 있다. 

 

 

또한 SecurityFIlterChain 내의 List<Filter>에는 시큐리티 설정에 따라 특정 필터가 추가되거나 제거된다. 실제로 formLogin, csrf 기능을 disabled한 SecurityFilterChain 내의 List<Filter>에는 해당 기능을 처리하는 CsrfFilter, UsernamePasswordAuthenticationFilter가 포함되지 않은 것을 확인할 수 있다. 

 

 

 

반면 "/", "/home" 요청을 처리하는 SecurityFilterChainList<Filter>에는 CsrfFilter, UsernamePasswordAuthenticationFilter가 포함되어 있다. 

 

 



정리하자면 Spring Security에서 모든 요청은 Filter Chain → DelegatingFilterProxy → FilterChainProxy → SecurityFilterChain → List<Filter>를 통하며 필터 단에서 모든 인증 및 인가 처리가 수행된다. 다음 글에서는 이 List<Filter>에 포함되는 핵심 필터들에는 어떠한 것들이 있으며 각 필터가 어떤 인증 및 인가 로직을 어떻게 처리하는지 알아보고자 한다.



끝.



참고

https://docs.spring.io/spring-security/reference/servlet/architecture.html
https://kasunprageethdissanayake.medium.com/spring-security-the-security-filter-chain-2e399a1cb8e3
https://www.baeldung.com/spring-delegating-filter-proxy
https://www.baeldung.com/spring-mvc-handlerinterceptor-vs-filter
https://howtodoinjava.com/spring-boot/spring-filter-examples/
https://www.baeldung.com/spring-boot-add-filter
https://mangkyu.tistory.com/221


공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함