Skip to Content
프로젝트원큐오더트러블슈팅Spring Security 필터 체인 등록 문제 해결하기

Spring Security 필터 체인 등록 문제 해결하기: 필터 순서의 중요성

결제를 요청하는데 사용되는 토큰을 검증하는 PaymentTokenAuthenticationFilterJwtAuthenticationFilter를 구성하다 마주친 문제와 이 오류의 원인을 파악하면서 Spring Security의 내부 동작에 대해 깊이 이해하게 된 경험을 공유합니다.

문제 상황: 왜 갑자기 필터 체인이 동작하지 않을까?

필터 체인 오류

2025년 5월 16일 로그인 API를 개발하며 JWT 인증 방식을 추가하기 위해 커스텀 필터를 설정했는데, 다음과 같은 오류가 발생했습니다:

[org.springframework.security.web.SecurityFilterChain]: Factory method 'securityFilterChain' threw exception with message: The Filter class com.fisa.pg.config.security.filter.PaymentTokenAuthenticationFilter does not have a registered order and cannot be added without a specified order

문제의 원인: 필터 순서는 생각보다 중요합니다

먼저 오류가 발생했던 코드는 다음과 같습니다:

SecurityConfig.java
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final PaymentAuthenticationFilter paymentAuthenticationFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http // ...중략... .addFilterBefore(jwtAuthenticationFilter, PaymentTokenAuthenticationFilter.class) // PaymentTokenAuthenticationFilter 필터 전에 JWT 필터 추가 .addFilterBefore(paymentTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // UsernamePasswordAuthenticationFilter 필터 전에 PaymentTokenAuthenticationFilter 필터 추가 .build(); } // ...중략... }

그리고 해결한 코드는 다음과 같습니다:

SecurityConfig.java
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final PaymentAuthenticationFilter paymentAuthenticationFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http // ...중략... .addFilterBefore(paymentTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // UsernamePasswordAuthenticationFilter 필터 전에 PaymentTokenAuthenticationFilter 필터 추가 .addFilterBefore(jwtAuthenticationFilter, PaymentTokenAuthenticationFilter.class) // PaymentTokenAuthenticationFilter 필터 전에 JWT 필터 추가 .build(); } // ...중략... }

왜 이전 코드에서 오류가 발생했을까요?

간단히 말하면, Spring Security는 필터 체인을 구성할 때 참조되는 필터가 이미 체인에 등록되어 있어야 합니다. 이전 코드에서는 PaymentTokenAuthenticationFilter가 아직 체인에 추가되지 않은 상태에서 참조했기 때문에 오류가 발생했습니다.

Spring Security 내부 들여다보기: 필터 체인은 어떻게 구성될까?

Spring Security가 어떻게 필터 체인을 구성하는지 코드 레벨에서 깊게 살펴보겠습니다. 이 과정을 이해하면 왜 필터 추가 순서가 중요한지 명확해질 것입니다.

1. 필터 순서 관리 방식

Spring Security는 FilterOrderRegistration이라는 클래스를 통해 필터의 순서를 관리합니다.

이 클래스는 각 필터의 순서를 filterToOrder라는 Map에 저장하는데, 표준 필터만 기본으로 등록되어 있습니다.

FilterOrderRegistration.java
final class FilterOrderRegistration { private static final int INITIAL_ORDER = 100; private static final int ORDER_STEP = 100; private final Map<String, Integer> filterToOrder = new HashMap<>(); FilterOrderRegistration() { put(DisableEncodeUrlFilter.class, order.next()); put(ForceEagerSessionCreationFilter.class, order.next()); put(ChannelProcessingFilter.class, order.next()); // ...중략... put(UsernamePasswordAuthenticationFilter.class, order.next()); // ...중략... put(FilterSecurityInterceptor.class, order.next()); put(AuthorizationFilter.class, order.next()); put(SwitchUserFilter.class, order.next()); } void put(Class<? extends Filter> filter, int position) { this.filterToOrder.putIfAbsent(filter.getName(), position); } // ...중략... }

그리고 커스텀 필터는 이 filterToOrder라는 Map에 등록되어 있지 않습니다.

2. 커스텀 필터 등록 과정

이제 표준 필터가 어떻게 등록되어 있는지 알았으니, 그 다음으로 우리가 직접 만든 커스텀 필터를 등록하는 과정을 살펴보겠습니다.

커스텀 필터를 등록할 때 사용하는 HttpSecurityaddFilterBefore 메서드는 다음과 같이 동작합니다:

HttpSecurity.java
public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity> implements SecurityBuilder<DefaultSecurityFilterChain>, HttpSecurityBuilder<HttpSecurity> { // ...중략... @Override public HttpSecurity addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter) { return addFilterAtOffsetOf(filter, -1, beforeFilter); } private HttpSecurity addFilterAtOffsetOf(Filter filter, int offset, Class<? extends Filter> registeredFilter) { Integer registeredFilterOrder = this.filterOrders.getOrder(registeredFilter); if (registeredFilterOrder == null) { throw new IllegalArgumentException("The Filter class " + registeredFilter.getName() + " does not have a registered order"); } int order = registeredFilterOrder + offset; this.filters.add(new OrderedFilter(filter, order)); this.filterOrders.put(filter.getClass(), order); return this; } // ...중략... }

실제 코드를 보면 addFilterBefore 메서드는 내부적으로 addFilterAtOffsetOf 메서드를 호출하고 있습니다. 이 메서드의 동작 방식을 단계별로 살펴보겠습니다:

  1. addFilterBefore는 새 필터를 특정 필터 ‘앞에’ 추가하려 할 때 addFilterAtOffsetOf를 offset -1로 호출합니다. (앞에 위치하려면 순서 값이 더 작아야 하기 때문입니다)

  2. addFilterAtOffsetOf 메서드는 다음 작업을 수행합니다:

    • filterOrders.getOrder(registeredFilter)를 호출해서 참조하는 필터의 순서를 조회합니다.
    • 만약 참조하는 필터가 등록되지 않았다면(순서가 null이면) 예외를 던집니다.
    • 참조 필터의 순서에 offset을 더해 새 필터의 순서를 계산합니다.
    • 새 필터를 OrderedFilter로 감싸서 filters 목록에 추가합니다.
    • 새 필터의 클래스와 계산된 순서를 filterOrders에 등록합니다. 이렇게 하면 나중에 이 필터를 참조할 수 있게 됩니다!

오류 다시 살펴보기: 근본 원인 분석

이제 Spring Security의 내부 코드를 분석했으니, 처음 만났던 오류를 다시 살펴보겠습니다:

The Filter class com.fisa.pg.config.security.filter.PaymentTokenAuthenticationFilter does not have a registered order and cannot be added without a specified order

이 오류 메시지는 PaymentTokenAuthenticationFilter 클래스가 등록된 순서(registered order)를 가지고 있지 않아서 발생했다고 말하고 있습니다.

.addFilterBefore(jwtAuthenticationFilter, PaymentTokenAuthenticationFilter.class)를 호출했을 때, PaymentTokenAuthenticationFilter가 아직 등록되지 않았기 때문에 filterOrders에서 순서를 찾을 수 없었고, 그 결과 예외가 발생한 것입니다.

해결 방법은 간단했습니다. PaymentTokenAuthenticationFilterUsernamePasswordAuthenticationFilter 필터 앞에 추가하고, JwtAuthenticationFilterPaymentTokenAuthenticationFilter 앞에 추가하는 것이었습니다.

마무리: 깊은 이해가 보안을 강화합니다

Spring Security의 필터 체인을 설정할 때는 단순히 코드를 작성하는 것 이상으로, 내부 메커니즘을 이해하는 것이 중요합니다. 이번 경험을 통해 Spring Security의 내부 동작 방식을 더 깊이 이해하게 되었고, 이를 바탕으로 더 안전한 결제 시스템을 구축할 수 있게 되었습니다.

참고 자료

Last updated on