
안녕하세요!
Spring Security는 웹 요청이 들어오는 순간부터 인증이 완료되는 순간까지,
정교한 필터 체인을 통해 보안을 철저히 지켜냅니다.
오늘은 이 과정에서 요청이 어떤 경로를 거치며, 어떤 핵심 모듈들이 협력하는지
단계별로 따라가 보겠습니다.
웹 요청은 서블릿 컨테이너에 도달하자마자 등록된 필터들을 차례로 통과합니다.
바로 여기서 Spring Security의 동작이 시작됩니다.
FilterChainProxy
→ Spring Security의 유일한 진입점 필터.
서블릿 컨테이너에 등록되며, 요청 URI에 맞는 보안 필터 체인을 선택해 실행합니다.
SecurityFilterChain
→ 실제 보안 로직을 수행하는 필터들의 묶음.
모든 요청은 DispatcherServlet에 도달하기 전에 이 체인을 반드시 통과하며,
인증, 인가, CSRF 방어 등 모든 보안 검사를 거칩니다.
SecurityFilterChain 안에는 수십 개의 필터가 순서대로 배치되어 있습니다.
그중 인증 흐름에서 특히 중요한 3가지 필터를 정리하면 다음과 같습니다.
| 필터 이름 | 주요 역할 |
|---|---|
UsernamePasswordAuthenticationFilter | /login 등 POST 요청에서 아이디/비밀번호를 추출해 인증 시도 |
ExceptionTranslationFilter | 인증/인가 실패 시 예외를 감지하고 적절한 응답(로그인 페이지 리다이렉트 등) 생성 |
FilterSecurityInterceptor | 체인의 마지막 관문. 요청에 대한 최종 인가(Authorization) 판단 |
UsernamePasswordAuthenticationFilter는 AbstractAuthenticationProcessingFilter 를 상속받아 동작합니다.
이 추상 클래스는 모든 폼 기반 인증 필터의 기본 뼈대를 제공합니다.
doFilter 메서드의 핵심 흐름private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 1. 이 요청이 로그인 처리 대상인가?
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// 2. 실제 인증 시도
Authentication result = attemptAuthentication(request, response);
// 3. 성공 → 후속 처리
successfulAuthentication(request, response, chain, result);
} catch (AuthenticationException failed) {
// 4. 실패 → 실패 핸들러 호출
unsuccessfulAuthentication(request, response, failed);
}
}
attemptAuthentication() → 성공 시 successfulAuthentication()
실패 시 unsuccessfulAuthentication()
이 조건부 분기 구조가 인증 흐름의 전부입니다.
successfulAuthentication()protected void successfulAuthentication(...) throws IOException, ServletException {
SecurityContext context = createEmptyContext();
context.setAuthentication(authResult); // ① 인증 정보 등록
securityContextHolderStrategy.setContext(context); // ② ThreadLocal 저장
securityContextRepository.saveContext(context, request, response); // ③ 세션 영속화
}
이 메서드는 인증된 Authentication 객체를
SecurityContext에 담고, SecurityContextHolder에 저장한 뒤, 팁:
SecurityContextHolder는 기본적으로 ThreadLocal 전략을 사용합니다.
즉, 현재 스레드 내에서만 인증 정보가 유지되며, 요청이 끝나면 자동 정리됩니다.
실제 아이디/비밀번호 검증은
UsernamePasswordAuthenticationFilter가 아니라
AuthenticationManager → AuthenticationProvider 가 담당합니다.
public Authentication attemptAuthentication(...) {
Authentication token = authenticationConverter.convert(request); // 아이디, 비밀번호 추출
if (authentication == null) {
return null;
}
Authentication result = this.authenticationManager.authenticate(authentication); // 위임
if (result == null) {
throw new ServletException("AuthenticationManager should not return null Authentication object.");
}
return result;
}
AuthenticationManager는 전달받은 인증 요청을 처리할 인증의 사령탑 역할을 합니다.
AuthenticationManager는 직접 인증 로직을 수행하지 않습니다. 대신, 내부적으로 등록된 여러 AuthenticationProvider 중에서 현재 UsernamePasswordAuthenticationToken 타입을 지원하는 Provider를 찾아 authenticate() 메서드를 호출합니다.@FunctionalInterface
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationProvider는 실제 인증 로직을 구현하는 객체입니다.
Authentication 객체를 AuthenticationManager에게 반환합니다.AuthenticationException을 던집니다.이 과정을 통해 인증된 Authentication 객체가 다시 필터로 돌아오고, successfulAuthentication()을 통해 SecurityContext에 저장되며 최종 인증이 완료됩니다.
public interface AuthenticationProvider {
/**
* 인증 수행:
* 요청된 {@code Authentication} 개체를 사용하여 인증을 시도하고, 성공하면 **완전히 인증된 객체**를 반환합니다.
*
* 지원하지 않는 요청이거나 (지원할 수는 있지만) Provider 내부적으로 인증을 처리하지 않기로 결정한 경우
* {@code null} 을 반환하여 {@code ProviderManager}가 다음 Provider를 시도하도록 합니다.
*
* @param authentication 인증 요청 개체입니다.
* @return 자격 증명을 포함하여 완전히 인증된 객체 또는 {@code null}
* @throws AuthenticationException 인증에 실패할 경우 발생합니다.
*/
Authentication authenticate(Authentication authentication) throws AuthenticationException;
/**
* 인증 지원 여부 확인:
* 이 Provider가 제시된 {@code Authentication} 클래스를 처리할 수 있는지 여부를 {@code true}로 반환합니다.
*
* 이 메서드가 {@code true}를 반환하더라도, 실제 인증 성공을 보장하지는 않으며,
* {@code authenticate()}에서 {@code null}을 반환하여 인증을 위임할 수 있습니다.
* Provider 선택은 {@code ProviderManager}가 런타임에 결정합니다.
*/
boolean supports(Class<?> authentication);
}
핵심 구현체인 AbstractUserDetailsAuthenticationProvider 폼 기반 인증에서 가장 많이 사용되는 기본 AuthenticationProvider 입니다.
내부 authenticate() 메서드는 아래 단계를 순차적으로 실행합니다:
UserDetailsService.loadUserByUsername() → DB 또는 메모리에서 사용자 조회 PasswordEncoder.matches(rawPassword, encodedPassword) → 비밀번호 일치 여부 확인 UsernamePasswordAuthenticationToken(principal, null, authorities) 생성 및 반환 BadCredentialsException 등 AuthenticationException 발생이렇게 AuthenticationProvider가 인증의 실질적인 책임자 역할을 수행함으로써,
필터는 오직 요청 가로채기와 결과 처리에만 집중할 수 있게 됩니다.
이제 하나의 로그인 요청이
FilterChainProxy → UsernamePasswordAuthenticationFilter → AuthenticationManager → AuthenticationProvider
를 거쳐 SecurityContextHolder까지 저장되는 과정이 명확해졌기를 바랍니다.
감사합니다.