제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁

스프링 정리를 위한 포스팅입니다.
해당 포스팅은 Spring Security DSL기반 필터 등록 과정입니다.

 

서론

문득 Security 작업을 진행하면서 커스텀 필터를 어느 곳에 위치하는 것이 좋은지 호기심이 생겼습니다.

처음 Security를 사용했을때 UsernamePasswordAuthenticationFilter를 앞에 문득 붙였었는데 

Spring Security는 다양한 필터를 체인 형태로 적용하는데, 내가 만든 필터가 정확히 어떤 순서에 들어가야 효율적인지,
이를 확인하기 위해 실제 Filter 등록과정을 분석하고, 내부 소스 코드를 따라가보았습니다.

 

 

 

본론

SecurityConfig 클래스는 다음과 같이 구성하였습니다.

@Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {

    http.formLogin()
            .loginPage("/login.html")            // 사용자 정의 로그인 페이지
            .defaultSuccessUrl("/home")          // 로그인 성공 후 이동 페이지
            .failureUrl("/login.html?error=true")          // 로그인 실패 후 이동 페이지
            .usernameParameter("username")       // 아이디 파라미터명 설정
            .passwordParameter("password")       // 패스워드 파라미터명 설정
            .loginProcessingUrl("/login");       // 로그인 Form Action Url

    return http
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement((sessionManagement) ->
                    sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> {
                auth.requestMatchers(PERMIT_ALL_URLS)
                        .permitAll();
                auth.requestMatchers(PERMIT_ADMIN_URLS)
                        .hasRole("ADMIN");
                auth.anyRequest()
                        .authenticated();
            })
            .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
//                .addFilterBefore(filter, LogoutFilter.class)
            .addFilter(webConfig.corsFilter())
            .build();
}

먼저 Security의 filter들이 어느 것이 있는지 파악하기 위해 다음과 같은 코드를 추가하였습니다.

import jakarta.servlet.Filter;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.FilterChainProxy;

import java.util.List;

@Configuration
public class FilterLoggingConfig {

    @Bean
    public ApplicationRunner logSecurityFilters(FilterChainProxy filterChainProxy) {
        return args -> {
            List<Filter> filters = filterChainProxy.getFilters("/");

            System.out.println("\n=== [Spring Security Filter Chain 순서] ===");
            for (int i = 0; i < filters.size(); i++) {
                System.out.printf("%2d. %s%n", i + 1, filters.get(i).getClass().getSimpleName());
            }
            System.out.println("==========================================\n");
        };
    }

}

스프링을 빌드하면 다음과 같은 출력이 나옵니다.

FilterChain 순서

여기서  7번째 JwtRequestFilter는 제가 작성한 커스텀 필터입니다.

UsernamePasswordAuthenticationFilter는 HttpSecurity.formLogin()시에 자동적으로 설정되는 필터입니다.
(Spring 6.1 이후 deprecated되었습니다. 예시를 위해 이해를 돕기 위한 구조로 설명을 진행하겠습니다.)

HttpSecurity 클래스의 formLogin

HttpSecurity 클래스에서는 formLogin() 메소드 호출시 FormLoginConfigurer 클래스를 생성 후 getOrApply 메소드의 인자 값으로 넘겨줍니다.

 

FormLoginConfigurer 생성자

 

FormLoginConfigurer는 AbstractAuthenticationFilterConfigurer를 상속받으며, 이 상위 클래스에서 기본 인증 필터인 UsernamePasswordAuthenticationFilter를 생성하고 등록하는 역할을 합니다.
이후 이 Configurer는 HttpSecurity 내부의 configurers 맵에 저장되어, 나중에 build() 과정에서 자동으로 configure() 메서드가 호출되며 필터가 SecurityFilterChain에 실제로 추가됩니다.

 

AbstractSecurityBuilder 추상클래스의 build() method
AbstractConfiguredSecurityBuilder 추상 클래스의 doBuild() method
HttpSecurity 클래스의 performBuild method

 

HttpSecurity.build()를 호출하면, 최종적으로 performBuild() 메서드가 실행됩니다. 이 메서드는 지금까지 설정된 모든 필터들을 종합하여 DefaultSecurityFilterChain 객체를 생성하는 핵심 단계입니다.
즉, 지금까지 Configurer들이 등록하고 추가한 필터들이 실제 하나의 FilterChain으로 묶여 Spring Security의 필터 체인으로 등록되는 마지막 절차입니다.

 

순서 정리

# 순서도

HttpSecurity
  └── formLogin()
       └── getOrApply()
            └── FormLoginConfigurer 생성
                 └── UsernamePasswordAuthenticationFilter 준비
HttpSecurity.build()
  └── doBuild()
       └── configure() 호출
            └── addFilter()
                 └── performBuild()
                      └── DefaultSecurityFilterChain 완성
  1. HttpSecurity.formLogin() 호출(HttpSecurity클래스)
    1. getOrApply(new FormLoginConfigurer<>()) 실행
      1. getConfigurer(configurer.getClass()) 실행
        1. getConfigurer(Class <C> clazz) 실행 (AbstractConfiguredSecurityBuilder 추상 클래스)
        2. 여기서 configures 정의
  2. FormLoginConfigurer(클래스 생성)
    1. AbstractAuthenticationFilterConfigurere에서 생성자 등록
      1. UsernamePasswordAuthenticationFilter 생성
  3. HttpSecurity.build() 호출 (SecurityBuilder 인터페이스)
    1. build() 호출 (AbstractSecurityBuilder 추상클래스)
      1. doBuild() 호출
        1. AbstractConfiguredSecurityBuilder 추상 클래스에서 doBuild() 실행
          1. configure() 실행
            1. AbstractConfiguredSecurityBuilder 클래스에서 호출. configurer.configure((B) this)
              1. HttpSecurity.addFilter()로 등록
          2. performBuild() 호출
            1. 여기서 filter가 등록된걸 DefaultSecurityFilterChain에 넣어주기(DefaultSecurityFilterChain)

결론

Spring Security에서 http.formLogin()을 호출하면, 내부적으로 FormLoginConfigurer가 HttpSecurity에 등록됩니다.

이 Configurer는 UsernamePasswordAuthenticationFilter를 생성한 뒤,

AbstractAuthenticationFilterConfigurer.configure()에서 http.addFilter()를 호출하여 필터 체인에 등록합니다.

이후 http.build() 호출 시 필터 리스트가 정렬되어 DefaultSecurityFilterChain이 생성되고,

최종적으로 FilterChainProxy에 포함되어 DispatcherServlet 앞에서 모든 요청을 처리하게 됩니다.

그래서 저는 UsernamePasswordAuthenticationFilter와 동일한 역할을 하는 Filter를 개발하여 해당 필터 순서에 넣었습니다.

 

용어정리

더보기

 

  • AbstractConfiguredSecurityBuilder: 여러 Configurer를 모아서 설정을 수행하는 빌더 클래스입니다.
  • FormLoginConfigurer: 로그인 처리를 위한 Configurer로, 기본적으로 UsernamePasswordAuthenticationFilter를 설정합니다.
  • DefaultSecurityFilterChain: 최종적으로 적용되는 필터 리스트를 가진 객체로, Spring Security가 실제 요청 처리 시 사용하는 체인입니다.
  • configures: HttpSecurity 설정시 다양한 설정 메소드 호출. 등록된 SecurityConfigurer들을 클래스 타입별로 그룹핑하여 저장하는 자료구조(formLogin(), csrf(), authorizeRequests() 등)

 

코드

 

+ Recent posts