본문 바로가기

CS

스프링 시큐리티 - 전체적인 흐름

 

개발자 유미의 시큐리티 내부 구조 영상을 보고 만들었습니다.

 


시큐리티 적용 전 :

이미지 출처: 개발자 유미

 

 

클라이언트가 웹 애플리케이션에 요청을 보내면, 해당 요청은 먼저 서버 컴퓨터에 도달하게 됩니다. 이 서버는 보통 웹 애플리케이션 서버 역할을 하는 소프트웨어를 통해 요청을 처리하게 되며, 대표적인 예로 톰캣이 있습니다.

 

톰캣은 요청을 처리할 때 가장 먼저 서블릿 필터 체인을 거칩니다. 필터는 요청과 응답을 가로채어 전처리나 후처리를 수행할 수 있는 컴포넌트로, 예를 들어 로그인 여부 검사, CORS 설정, 로깅, 보안 검사, 인코딩 설정 등이 여기에 해당합니다. 여러 개의 필터가 존재할 수 있으며, 설정된 순서대로 하나씩 실행됩니다.

 

필터를 모두 통과한 요청은 그 다음에 서블릿으로 전달되고, Spring Boot 기반 애플리케이션의 경우 이 시점에서 Spring DispatcherServlet이 요청을 넘겨받게 됩니다. DispatcherServlet은 스프링 프레임워크의 중심 서블릿으로, 요청을 적절한 컨트롤러로 매핑하고, 그 컨트롤러가 요청을 처리하게 합니다.


시큐리티 적용 후 :

이미지 출처: 개발자 유미

시큐리티 의존성이 추가되면, 위의 기본 흐름에 보안 로직이 개입하게 됩니다. 시큐리티는 사용자의 요청을 감시하고 통제하기 위해 서블릿 필터 단계에 자체적인 필터를 삽입합니다.

 

이 필터는 단순한 일반 필터가 아닌, DelegatingFilterProxy라는 특수한 프록시 필터로 등록됩니다. DelegatingFilterProxy는 실제로는 아무 작업도 하지 않고, Spring 컨테이너 내부에 정의된 FilterChainProxy라는 빈에게 요청을 위임합니다.

 

이미지 출처: 개발자 유미

 

 

FilterChainProxy는 다시 여러 개의 보안 필터들이 연결된 Security Filter Chain을 실행합니다. 이 체인에는 인증, 인가, CSRF 보호, 세션 관리, 로그아웃 처리 등 다양한 보안 기능이 필터 단위로 구현되어 있습니다. 요청은 이 보안 필터들을 순차적으로 거치며, 필요한 보안 검증을 수행합니다.

 

보안 검증이 끝나면 요청은 다시 DelegatingFilterProxy를 통해 서블릿 필터 체인의 다음 필터로 복귀하고, 이후 DispatcherServlet으로 전달되어 컨트롤러까지 도달하게 됩니다. 이 과정에서 사용자가 인증되지 않았거나 권한이 없을 경우, Security 필터에서 요청이 차단되거나 리다이렉트되며 컨트롤러까지 도달하지 못할 수도 있습니다.

 

결과적으로 시큐리티가 적용되면, 클라이언트의 요청은 컨트롤러에 도달하기 전에 반드시 스프링 시큐리티의 보안 필터 체인을 통과하게 되며, 이 필터 체인을 통해 요청에 대한 접근 제어와 보안 정책이 선제적으로 적용됩니다.


DelegatingFilterProxy

 

DelegatingFilterProxy는 시큐리티의 핵심 구성 요소 중 하나로, WAS의 서블릿 필터 체계와 스프링 컨테이너 내부의 시큐리티 필터 체인을 연결해주는 중간 다리 역할을 합니다. 쉽게 말해, 서블릿 필터로 등록되긴 하지만 실제 동작은 스프링 빈으로 등록된 보안 필터 체인이 수행합니다. 이 구조를 통해 스프링 시큐리티는 톰캣의 필터 체계에 자연스럽게 통합되면서도, 내부적으로는 스프링 빈으로 관리되는 유연한 필터 체인을 구성할 수 있게 됩니다.

 

스프링에서는 시큐리티 의존성을 추가하면 SecurityAutoConfiguration과 SecurityFilterAutoConfiguration 클래스가 자동으로 동작하며, DelegatingFilterProxyRegistrationBean을 통해 DelegatingFilterProxy가 WAS의 필터 체인에 등록됩니다.

 

이때 등록되는 필터의 이름은 고정값인 springSecurityFilterChain이며, 이 이름은 DelegatingFilterProxy가 내부적으로 위임할 Bean의 이름과 정확히 일치해야 합니다. 그렇기에 시큐리티가 보안 필터 체인을 정의할 때, 반드시 이름을 "springSecurityFilterChain"으로 등록해 줍니다. 그래야 DelegatingFilterProxy가 해당 Bean을 찾아서 요청을 넘길 수 있기 때문입니다.

 

 

 

다음은 DelegatingFilterProxy 등록 과정의 핵심 코드입니다

@Bean
@ConditionalOnBean(name = DEFAULT_FILTER_NAME) // springSecurityFilterChain
public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
        SecurityProperties securityProperties) {

    DelegatingFilterProxyRegistrationBean registration =
        new DelegatingFilterProxyRegistrationBean(DEFAULT_FILTER_NAME); // "springSecurityFilterChain"

    registration.setOrder(securityProperties.getFilter().getOrder()); // 필터 실행 순서 지정
    registration.setDispatcherTypes(getDispatcherTypes(securityProperties)); // 요청 종류 설정

    return registration;
}

 

결과적으로 DelegatingFilterProxy는 자체적으로 보안 로직을 수행하지 않으며, 실제로 요청을 처리하는 것은 Spring Security가 제공하는 FilterChainProxy입니다. DelegatingFilterProxy는 단지 해당 빈을 찾아 요청을 위임하는 프록시 역할만을 담당합니다. 

 

WAS 필터 체인 → DelegatingFilterProxy → FilterChainProxy (springSecurityFilterChain 빈) → SecurityFilterChain (여러 보안 필터들)

 


SecurityFilterChain 

 

SecurityFilterChain은 시큐리티에서 요청을 처리하기 위한 보안 필터들의 묶음을 의미합니다. 하나의 필터 체인은 특정 URL 패턴에 대해 작동하며, 인증, 인가, CSRF 등 다양한 보안 필터들을 포함합니다. 스프링 시큐리티 의존성을 추가하면 기본적으로 하나의 DefaultSecurityFilterChain이 등록되며, 이 체인이 모든 요청에 대해 작동합니다.


커스텀 SecurityFilterChain 등록 :

 

사용자가 원하는 보안 정책을 적용하기 위해, Bean으로 SecurityFilterChain을 직접 등록할 수 있습니다. 이 방식은 하나 이상의 필터 체인을 정의할 수 있도록 허용하며, 각 체인마다 다른 URL 경로와 보안 설정을 지정할 수 있습니다.

 

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain1(HttpSecurity http) throws Exception {
        http
            .securityMatchers(auth -> auth.requestMatchers("/user"))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/user").permitAll());

        return http.build();
    }

    @Bean
    public SecurityFilterChain filterChain2(HttpSecurity http) throws Exception {
        http
            .securityMatchers(auth -> auth.requestMatchers("/admin"))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin").authenticated());

        return http.build();
    }
}

 

우선 우선순위의 기존은 등록 인덱스 순이며, 그 다음으로 필터 체인에 대한 RequstMatcher 값이 일치하는지를 확인하는 것으로 이루어졌습니다.

 

멀티 필터 체인을 정의하면서 securityMatchers(...) 설정을 생략하면, 모든 체인이 기본적으로 "/" 요청을 처리하게 됩니다. 이로 인해 먼저 등록된 체인이 해당 요청을 처리하게 되며, 이후 체인은 무시됩니다.

 

예를 들어, /admin 요청이 첫 번째 체인에서 처리되지만, 그 체인에 /admin에 대한 인가 설정이 없다면 요청은 거부됩니다. 이를 방지하려면 반드시 securityMatchers를 사용해 각 필터 체인의 작동 경로를 명확히 지정해야 합니다.

 


정적 자원 무시 처리 (필터 건너뛰기)

정적 자원(css, js, 이미지 등)은 보안 검사를 하지 않아도 되므로 필터 체인을 아예 거치지 않도록 설정할 수 있습니다. 이때는 WebSecurityCustomizer를 사용합니다.

 

이 설정이 적용되면, /img/** 등의 요청은 Spring Security 필터 체인을 전혀 통과하지 않고, 곧바로 컨트롤러 혹은 정적 자원 핸들러로 전달됩니다.

 
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
    return web -> web.ignoring().requestMatchers("/img/**", "/css/**", "/js/**");
}