[Spring] Spring Security 인증 흐름, 필터부터 Provider까지 정리

조시현·2025년 11월 6일
post-thumbnail

안녕하세요!

Spring Security는 웹 요청이 들어오는 순간부터 인증이 완료되는 순간까지,
정교한 필터 체인을 통해 보안을 철저히 지켜냅니다.
오늘은 이 과정에서 요청이 어떤 경로를 거치며, 어떤 핵심 모듈들이 협력하는지
단계별로 따라가 보겠습니다.

1. Spring Security의 시작점: SecurityFilterChain

웹 요청은 서블릿 컨테이너에 도달하자마자 등록된 필터들을 차례로 통과합니다.
바로 여기서 Spring Security의 동작이 시작됩니다.

  • FilterChainProxy
    → Spring Security의 유일한 진입점 필터.
    서블릿 컨테이너에 등록되며, 요청 URI에 맞는 보안 필터 체인을 선택해 실행합니다.

  • SecurityFilterChain
    → 실제 보안 로직을 수행하는 필터들의 묶음.
    모든 요청은 DispatcherServlet에 도달하기 전에 이 체인을 반드시 통과하며,
    인증, 인가, CSRF 방어 등 모든 보안 검사를 거칩니다.

2. 핵심 보안 필터 한눈에 보기

SecurityFilterChain 안에는 수십 개의 필터가 순서대로 배치되어 있습니다.
그중 인증 흐름에서 특히 중요한 3가지 필터를 정리하면 다음과 같습니다.

필터 이름주요 역할
UsernamePasswordAuthenticationFilter/login 등 POST 요청에서 아이디/비밀번호를 추출해 인증 시도
ExceptionTranslationFilter인증/인가 실패 시 예외를 감지하고 적절한 응답(로그인 페이지 리다이렉트 등) 생성
FilterSecurityInterceptor체인의 마지막 관문. 요청에 대한 최종 인가(Authorization) 판단

3. 인증의 핵심 엔진: AbstractAuthenticationProcessingFilter

UsernamePasswordAuthenticationFilterAbstractAuthenticationProcessingFilter 를 상속받아 동작합니다.
이 추상 클래스는 모든 폼 기반 인증 필터의 기본 뼈대를 제공합니다.

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 객체

  1. SecurityContext에 담고,
  2. SecurityContextHolder에 저장한 뒤,
  3. 세션에 영속화하여 다음 요청에서도 사용 가능하게 만듭니다.

: SecurityContextHolder는 기본적으로 ThreadLocal 전략을 사용합니다.
즉, 현재 스레드 내에서만 인증 정보가 유지되며, 요청이 끝나면 자동 정리됩니다.


4. 인증의 실질적 처리: Manager → Provider 협업

실제 아이디/비밀번호 검증
UsernamePasswordAuthenticationFilter가 아니라
AuthenticationManagerAuthenticationProvider 가 담당합니다.


① 필터 단계: 토큰 생성 및 위임

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는 전달받은 인증 요청을 처리할 인증의 사령탑 역할을 합니다.

  • 위임: AuthenticationManager는 직접 인증 로직을 수행하지 않습니다. 대신, 내부적으로 등록된 여러 AuthenticationProvider 중에서 현재 UsernamePasswordAuthenticationToken 타입을 지원하는 Provider를 찾아 authenticate() 메서드를 호출합니다.
  • 역할: 인증 요청을 적합한 Provider에게 라우팅하고, Provider로부터 받은 결과를 다시 필터에게 반환하는 역할을 합니다.
@FunctionalInterface
public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) 
            throws AuthenticationException;
}

③ AuthenticationProvider: 실질적 인증 수행

AuthenticationProvider실제 인증 로직을 구현하는 객체입니다.

  • Provider는 전달받은 토큰 정보(아이디, 비밀번호)를 기반으로 사용자 정보를 DB에서 조회하고, 비밀번호를 검증하는 등의 핵심 작업을 수행합니다.
  • 성공 시: 사용자 정보와 권한이 모두 채워진 완전히 인증된 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() 메서드는 아래 단계를 순차적으로 실행합니다:

  1. UserDetailsService.loadUserByUsername() → DB 또는 메모리에서 사용자 조회
  2. PasswordEncoder.matches(rawPassword, encodedPassword) → 비밀번호 일치 여부 확인
  3. 성공UsernamePasswordAuthenticationToken(principal, null, authorities) 생성 및 반환
  4. 실패BadCredentialsExceptionAuthenticationException 발생

이렇게 AuthenticationProvider인증의 실질적인 책임자 역할을 수행함으로써,
필터는 오직 요청 가로채기와 결과 처리에만 집중할 수 있게 됩니다.


마무리

이제 하나의 로그인 요청이
FilterChainProxyUsernamePasswordAuthenticationFilterAuthenticationManagerAuthenticationProvider
를 거쳐 SecurityContextHolder까지 저장되는 과정이 명확해졌기를 바랍니다.

감사합니다.

profile
Luck favors the prepared. Chance favors the prepared mind

0개의 댓글