이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 요청은 왜 Controller로 바로 가지 않고 Servlet Container와 FilterChain을 먼저 지나는가?
DelegatingFilterProxy,FilterChainProxy,SecurityFilterChain은 각각 어떤 위치에 있는가?- 401, 403, 302, CORS, CSRF 403에서 Controller보다 앞단을 먼저 봐야 하는 이유는 무엇인가?
개요
Spring Security를 이해하는 첫 문장은 이것이다. 클라이언트 요청은 Controller로 바로 가지 않는다. 요청은 먼저 Servlet Container에 도착하고, Container는 요청에 맞는 FilterChain을 실행한다. Spring Security는 이 FilterChain 구간에서 동작한다.
기존 Security 문서의 핵심도 이 문장에 있었다. Controller breakpoint가 걸리지 않는 보안 문제는 Controller 코드가 아니라 Security FilterChain에서 이미 응답이 결정되었을 가능성이 크다.
요청 흐름
Client
-> Servlet Container
-> Servlet FilterChain
-> Spring Security FilterChain
-> DispatcherServlet
-> Controller
-> Service
-> RepositoryServlet Container는 HTTP 요청을 받아 HttpServletRequest와 HttpServletResponse를 만든다. 그 다음 요청 URI에 맞는 Filter들을 순서대로 실행한다. Filter는 요청을 다음 필터나 Servlet으로 넘길 수도 있고, 직접 응답을 작성하고 흐름을 멈출 수도 있다.
Spring MVC에서 Servlet은 DispatcherServlet이다. 하지만 DispatcherServlet은 FilterChain 뒤에 있다. 그래서 인증 실패, 권한 부족, CSRF 실패, form login redirect, CORS 처리 같은 일이 Controller 이전에 끝날 수 있다.
Security 연결 구조
DelegatingFilterProxy는 Servlet Container의 Filter와 Spring Bean 세계를 연결한다. Servlet Container는 Spring Bean을 직접 알지 못하므로, proxy filter가 Spring ApplicationContext의 filter bean으로 작업을 위임한다.
FilterChainProxy는 Spring Security의 중심 Filter다. 이 객체가 여러 SecurityFilterChain 중 현재 요청에 맞는 체인을 선택하고, 그 안에 등록된 보안 필터들을 실행한다. 이 위치가 Spring Security Servlet 지원의 시작점이므로, 필터 체인 디버깅도 FilterChainProxy 로그를 보면 가장 빠르다.
SecurityFilterChain은 특정 요청에 적용될 Security Filter 목록이다. /api/**와 /admin/**에 서로 다른 체인을 둘 수 있으며, 첫 번째로 매칭된 체인만 실행된다는 점이 중요하다. 어떤 체인은 필터가 거의 없거나 아예 없게 구성될 수도 있지만, 정적 리소스도 보안 헤더 같은 처리가 필요하면 완전히 ignore하기보다 permitAll을 우선 검토한다. 단, permitAll은 인가를 허용하는 규칙이지 CSRF나 다른 보안 필터 전체를 우회한다는 뜻은 아니다.
코드 예제
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
}이 설정은 /api/** 요청에 적용될 SecurityFilterChain을 만든다. 요청이 Controller에 도착하기 전에 이 체인에서 인증과 인가가 먼저 판단된다.
증상 해석
401은 인증 정보가 없거나 유효하지 않을 때 자주 나온다. Bearer prefix 누락, JWT 만료, 서명 검증 실패, Basic 인증 누락이 원인일 수 있다.
403은 인증은 되었지만 권한이 부족하거나 CSRF token이 없는 경우에 자주 나온다. role prefix 문제도 흔하다.
302는 API인데 form login이 활성화되어 로그인 페이지로 redirect될 때 볼 수 있다. REST API는 entry point를 명시해 JSON 오류나 401을 반환하도록 조정해야 할 수 있다.
CORS 오류는 브라우저 preflight 요청이 Security 또는 CORS 설정에서 허용되지 않을 때 발생한다. Controller에 들어가기 전 OPTIONS 요청이 막히는지 확인한다.
CSRF 403은 state-changing 요청에서 token이 없거나 틀릴 때 흔하다. 단순 권한 부족 403과 같은 status로 보이므로 Security DEBUG 로그에서 CsrfFilter가 원인인지 확인해야 한다.
실전 팁
- Controller breakpoint가 안 걸리면 FilterChain부터 본다.
- DEBUG 로그에서 어떤 SecurityFilterChain이 요청을 보호하는지 확인한다.
- 여러 SecurityFilterChain을 쓸 때는 matcher 순서를 좁은 것에서 넓은 것으로 둔다.
- static resource는 보안 필터를 완전히 우회시키기보다
permitAll로 통과시키는 것이 필요한 보안 헤더를 유지하는 데 유리할 수 있다. - 커스텀 필터는 “어느 필터 앞/뒤”에 둘지 이유를 적는다.
- API 서버에서는 form login redirect가 필요한지 먼저 검토한다.
위험 신호!
- 401/403을 Controller 예외 처리에서 잡으려고 한다.
- 여러 SecurityFilterChain의 matcher 순서가 넓은 규칙부터 시작한다.
- 커스텀 JWT 필터가 AuthorizationFilter 뒤에 들어간다.
- CSRF 403과 권한 부족 403을 로그 없이 같은 원인으로 처리한다.
- CORS preflight를 Controller 매핑 문제로만 본다.
chain.doFilter()호출 여부를 확인하지 않고 필터를 작성한다.
확인 질문
확인 질문
- Spring MVC에서 Controller 앞에 있는 Servlet 구성요소는 무엇인가?
- Servlet Container와 FilterChain, 그리고 Spring Security FilterChain이 있다.
FilterChainProxy의 역할은 무엇인가?
- 현재 요청에 맞는 SecurityFilterChain을 선택하고 그 안의 보안 필터들을 실행하는 것이다.
- Controller breakpoint가 걸리지 않는 403에서 먼저 볼 것은 무엇인가?
- Security FilterChain, CSRF, 권한 규칙, 인증 상태를 먼저 봐야 한다.