본문 바로가기

스프링 시큐리티 흐름 분석: DelegatingFilterProxy와 인증 체계 이해하기

@6uiw2025. 10. 28. 13:06


🐣 사전 지식

  • 인증(Authentication) : 사용자가 누구인지 확인하는 단계 (ex: 로그인)
  • 인가(Authorization): 리소스 접근 권한을 확인
  • 접근주체(principal) : 애플리케이션의 기능을 사용하는 주체

스프링 시큐리티

  • 애플리케이션의 인증, 인가 등의 보안 기능을 제공하는 스프링 하위 프로젝트 중 하나
  • 서블릿 필터 기반 동작

 

 

시큐리티 필터체인이 실행되는 과정

 

 

ApplicationFilterChain 부분을 자세히 살펴보자.

DelegatingFilterProxy
스프링 시큐리티에서 사용하고자 하는 필터체인을 끼워넣기 위해 사용하는 proxy
서블릿 컨테이너 생명주기와 스프링 애플리케이션 컨텍스트 사이에서 다리 역할을 수행하는 필터 구현체

 

 

클라이언트가 요청을 보내면 서블릿 컨테이너(ex: Tomcat)에서 요청을 받고, 등록된 필터들을 차례로 실행한다.

필터체인에서 DelegatingFilterProxy가 실행되면, 내부에서 Spring Context안의 FilterChainProxy란 빈을 찾아 스프링 시큐리티 필터체인의 실행을 위임하고, FilterChainProxy가 요청 URL에 따라 해당하는 SecurityFilterChain을 찾아 필터들을 차례로 실행시킨 후 DispatcherServlet으로 넘어감

 

 

 

 

 

 

 


 

UsernamePasswordAuthenticationFilter 를 통한 인증 과정

순서대로 살펴보자.

 

 

1. Http Request → AuthenticationFilter

 

클라이언트가 로그인 요청을 보내면 스프링 시큐리티 체인 내의 인증 필터요청을 가로챔

UsernamePasswordAuthenticationFilter(폼 로그인)
BasicAutrhenticationFilter (HTTP Basic)
커스텀 JwtAuthenticationFilter 등..

 

여기서 username, password 등(자격증명 요소)을 추출 → 요소를 담아 토큰 생성

String username = request.getParameter("username");
String password = request.getParameter("password");
UsernamePasswordAuthenticationToken token =
    new UsernamePasswordAuthenticationToken(username, password);

 

 

 

 

 

 

2. AuthenticationFilter → AuthenticationManager.Authenticate(..)

필터는 토큰을 AuthenticationManager에 넘겨 인증 요청

Authentication authResult = authenticationManager.authenticate(token);

 

AuthenticationManager의 기본 구현은 ProviderManager이고, 내부에 여러 AuthenticationProvider을 가지고 있다.

 

 

 

 


💡 참고 AuthenticationManager vs ProviderManager vs AuthenticationProvider

AuthenticationManager (인터페이스)
│
▼
ProviderManager (AuthenticationManager 구현체)
│
├─ AuthenticationProvider1
├─ AuthenticationProvider2
└─ AuthenticationProvider3

 

AuthenticationManager

  • 정의: 스프링 시큐리티에서 인증 요청(Authentication)을 받아 인증을 수행하는 최상위 인터페이스
  • 역할: 클라이언트의 로그인 정보가 유효한지 검증
  • 구현체: 대부분 ProviderManager
  • 관점: "인증 요청을 받아 적절한 인증 처리기에게 전달하고 결과를 돌려주는 매니저"
클라이언트 -> AuthenticationManager.authenticate(Authentication) -> 인증 처리

 

 

ProviderManager

  • 정의: AuthenticationManager 인터페이스를 구현한 대표적인 클래스
  • 역할: 내부에 여러 AuthenticationProvider를 가지고 있음
  • 동작 흐름:
    1. 인증 요청(Authentication 객체)을 받음
    2. 등록된 AuthenticationProvider 리스트 순회
    3. supports()가 true인 Provider 찾음
    4. 해당 Provider의 authenticate() 호출
    5. 인증 성공하면 반환, 실패 시 다음 Provider 시도
ProviderManager
 ├─ DaoAuthenticationProvider (폼 로그인)
 ├─ JwtAuthenticationProvider (JWT 로그인)
 └─ OAuth2AuthenticationProvider (OAuth 로그인)

 

 

AuthenticationProvider

  • 정의: 실제 인증 로직을 수행하는 객체
  • 핵심 메서드:
    • supports(Class<?> authentication) : 이 Provider가 이 인증 요청을 처리할 수 있는지 확인
    • authenticate(Authentication authentication) : 실제 인증 수행
  • 예시:
    • DaoAuthenticationProvider → DB에서 사용자 조회 후 비밀번호 확인
    • JwtAuthenticationProvider → JWT 토큰 검증 후 사용자 정보 추출
    • OAuth2AuthenticationProvider → OAuth2 서버에서 인증

 

 


다시 본론으로 ..

3. ProviderManager -> 각 AuthenticationProvider 시도

ProviderManager는 등록된 AuthenticationProvider들을 순회하면서 supports()가 참인 Provider에 authenticate()를 위임한다.

  • 로그인 방식에 따라 매칭
ex) JwtAuthenticationToken -> JwtAuthenticationProvider
      OAuth2 -> OAuth2AuthenticationProvider)

 

 

 

 

 

4. AuthenticationProvider -> UserDetailsService로 사용자 정보 로드

UserDetails userDetails = userDetailsService.loadUserByUsername(username);

 


💡참고

UserDetails

UserDetails는 getUsername()getPassword()getAuthorities() 등 "한 명의 사용자 정보"를 제공하는 인터페이스다. (보통 User 엔티티를 감싸서 구현)
<스프링에서 제공하는 UserDetails 인터페이스> 
public interface UserDetails extends Serializable {

	Collection<? extends GrantedAuthority> getAuthorities(); //권한 목록

	String getPassword(); //비밀번호

	String getUsername(); //이름(아이디)

	default boolean isAccountNonExpired() {return true;} //계정 만료여뷰

	default boolean isAccountNonLocked() {return true;} //계정 잠금 여부

	default boolean isCredentialsNonExpired() {return true;} //비밀번호 만료여부

	default boolean isEnabled() {return true;} //계정 활성화 여부

}

 

UserDetailsService

스프링 시큐리티의 인증 과정에서 사용자 정보를 가져오기 위한 인터페이스

아래의 코드에서 볼 수 있듯이 usrename을 받아 그 사용자 정보를 담은 UserDetails객체를 반환하는 게 목적

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

 

보통 직접 커스텀해서 구현 

 

예시 UserDetailsService 구현:

@Service
public class MyUserDetailsService implements UserDetailsService {
  @Override
  public UserDetails loadUserByUsername(String username) {
  
  	//사용자 정보 조회 
    User user = userRepository.findByUsername(username)
                 .orElseThrow(() -> new UsernameNotFoundException(username));
                 
    //UserDetails 객체로 변환 후 반환
    return new org.springframework.security.core.userdetails.User(
        user.getUsername(), user.getPassword(), user.getAuthorities());
  }
}

5. 인증 결과를 SecurityContextHolder에 저장

인증 성공 시, AuthenticationFilter(혹은 시큐리티의 핸들러)가 SecurityContextHolder.getContext().setAuthentication(authResult) 를 호출한다.

  • SecurityContextHolder는 기본적으로 ThreadLocal(스레드당) 전략을 사용해서 현재 쓰레드에서 접근 가능한 SecurityContext를 제공한다.
  • 또한 HttpSessionSecurityContextRepository가 사용되면 SecurityContext는 HttpSession에 저장되어(세션 기반 인증) 다음 요청에서 재사용 가능하다.
  • 결과적으로 이후 필터나 컨트롤러에서 SecurityContextHolder.getContext().getAuthentication()으로 인증정보(Principal, Authorities 등)를 조회할 수 있다.
6uiw
@6uiw :: LOG.INFO("MING's DEVLOG")

개발을 하면서 공부한 기록을 남깁니다

목차