1. 개요
현재 프로젝트에서 Spring Security로 세션-쿠기 기반 인증을 사용하며, Spring SecurityFilterChain을 커스터마이징하여 REST API 방식으로 인증을 처리하고 있다.
Form 로그인 방식을 사용하지 않고, AbstractAuthenticationProcessingFilter를 상속받은 커스텀 인증 필터( ApiAuthenticationFilter)를 UsernamePasswordAuthenticationFilter 이전에 실행되도록 등록했다.
SecurityFilterChain 구성 예시
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(request -> request
.requestMatchers(LOGIN_URL).permitAll()
.anyRequest().authenticated())
.addFilterBefore(apiAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(entryPoint) // 자격 증명 요청 담당(인증 되지 않은 클라이언트)
.accessDeniedHandler(deniedHandler) // 액세스 거부 처리(인증은 되었지만 권한이 없는 클라이언트)
)
.logout(logout -> logout
.logoutUrl(LOGOUT_URL)
.logoutSuccessHandler(logoutSuccessHandler) // 로그아웃 성공 핸들러
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("JSESSIONID")
);
return http.build();
}
2. 문제점
AuthenticationEntryPoint는 인증되지 않은 사용자가 접근했을 때 호출되어, 인증 절차를 유도하는 기능을 한다.
기본적으로 로그인 페이지로 리다이렉트 하는 용도이지만, 여기서는 커스텀하게 구성하여 로그인이 필요하다는 API 응답을 하도록 구성했다.
LogoutSucessHandler는 로그아웃 요청이 정상 처리된 후 로그아웃 성공을 응답을 생성하도록 구현되어 있다.
로그아웃 요청이 들어와 LogoutFilter에서 처리 절차를 완료한 후 실행된다.
문제 상황
여기서 발생한 문제는 로그인하지 않은 사용자가 로그아웃 API를 호출하면 LogoutSucessHandler를 호출되는 것이었다.
나의 예상은 로그인 하지 않은 유저는 자격이 없어, AuthenticationEntryPoint(entryPoint)가 호출되는 것이었다.
즉, 로그아웃 요청 시 인증되지 않은 사용자임에도 AuthenticationEntryPoint가 호출되지 않는 것이다.
시큐리티 아키텍처에 대해서 공부가 부족해서 어떤 식으로 동작하는지 이해할 수가 없었다.
SecurityFilterChain이 정확히 어떻게 동작하는지 파악해 보자.
3. 문제 원인 파악
왜 로그아웃 API는 로그아웃 핸들러부터 호출되는 것일까?
내가 원하는 것은 로그인하지 않은 클라이언트는 자격 증명이 안된 상태여서 AuthenticationEntryPoint가 호출되는 것인데!!!
등록된 필터를 확인
logging 레벨을 설정하여 등록된 필터를 로그로 확인해 보자.
logging:
level:
org.springframework.security: debug
로그 확인
2025-08-13T19:44:56.582+09:00 DEBUG 16864 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with filters: DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, LogoutFilter, ApiAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, ExceptionTranslationFilter, AuthorizationFilter
- 로그를 확인해 보니 등록된 필터는 아래와 같다.
- DisableEncodeUrlFilter
- WebAsyncManagerIntegrationFilter
- SecurityContextHolderFilter
- LogoutFilter
- ApiAuthenticationFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- AnonymousAuthenticationFilter
- ExceptionTranslationFilter
- AuthorizationFilter
필터 순서 확인
필터 순서를 확인하려면 FilterOrderRegistration 클래스에 대해서 알아볼 필요가 있다.
FilterOrderRegstration
- FilterChain에서 실행되는 기본 순서를 정의하는 코드이다.
- INITIAL_ORDER = 100으로 시작해서, ORDER_STEP = 100씩 증가시켜 필터 순서를 부여한다.
- put(Filter.class, order.next()); 형태로 등록된 순서대로 필터 체인에서 실행된다.
- Spring Security에서 커스텀 필터를 추가할 때 addFilterBefore / addFilterAfter / addFilterAt이 바로 이 순서를 기반으로 동작한다.
@SuppressWarnings("serial")
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() {
Step order = new Step(INITIAL_ORDER, ORDER_STEP);
put(DisableEncodeUrlFilter.class, order.next());
// 중략..
put(WebAsyncManagerIntegrationFilter.class, order.next());
put(SecurityContextHolderFilter.class, order.next());
put(LogoutFilter.class, order.next());
// 중략 ..
put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
// 중략 ..
put(UsernamePasswordAuthenticationFilter.class, order.next());
// 중략..
put(RequestCacheAwareFilter.class, order.next());
put(SecurityContextHolderAwareRequestFilter.class, order.next());
// 중략 ..
put(AnonymousAuthenticationFilter.class, order.next());
// 중략..
put(ExceptionTranslationFilter.class, order.next());
// 중략..
put(AuthorizationFilter.class, order.next());
// 중략..
}
}
필터 등록 순서를 보니 자격 증명을 하는 ExceptionTranslationFilter가 LogoutFilter보다 뒤에 등록되어 있다.
즉, LogoutFilter가 먼저 실행되어서 LogoutSucessHandler가 먼저 동작되는 것이었다.
4. 해결 방안
이번 문제를 해결하기 위해 2가지 방법을 고려했다.
1. LogoutFilter 제거 후 커스텀 구현
logout() 설정을 제거하여, 컨트롤러로 직접 구현하거나, 필터 체인의 실행 순서를 재설계하여 해결하는 방법이다.
이 방법은 구현이 많아져 부담...
2. 기존 LogoutSucessHandler 로직 수정
LogoutSucessHandler 내부 인증 객체가 없는 경우의 예외 처리 로직을 추가하는 것이다.
첫번째 방식보다 단순하고, SecurityFilterChain을 그대로 유지할 수 있다.
이 방식을 선택함..
5. 마무리
이번 시간을 통해 spring seucirty의 아키텍처에 대해서 조금 이해할 수 있었고,
SecurityFilterChain에 등록된 필터들이 어떤 순서로 등록되고 실행되는지 정확히 파악할 수 있었다.
참고 자료
Architecture :: Spring Security
The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API. Those filters can be used for a number of different purposes, like exploit protection, authentication, authorization, and more. The filters are executed in a spec
docs.spring.io
spring-security/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java at
Spring Security. Contribute to spring-projects/spring-security development by creating an account on GitHub.
github.com
'Programming > Spring' 카테고리의 다른 글
| [Spring] Filter vs Interceptor 차이 (0) | 2025.10.20 |
|---|---|
| [Spring] Spring MVC에서 Pageable 파라미터가 동작하는 방식 (1) | 2025.09.16 |
| [Spring 테스트] Mockito ArguementCaptor란? (1) | 2025.08.10 |
| [Spring 테스트] 테스트 간 데이터 충돌 문제 해결(@Sql 이해) (0) | 2025.07.11 |
| [JPA] 실전! 스프링 부트와 JPA 활용2 강의 핵심 내용 정리 (0) | 2025.07.01 |