Spring Security 공식 문서 정리 - 아키텍처

JHLee·2025년 8월 7일

Spring Security

목록 보기
1/1
post-thumbnail

📌 본 글은 Spring Security 공식 문서 를 기반으로 정리한 글입니다.
Spring Security의 필터 체인 구조, DelegatingFilterProxy의 동작 방식, 요청 저장 및 예외 처리 흐름 등을 예시 코드와 함께 설명합니다.
단, 공식 문서 전체를 번역하거나 완전히 포함한 글은 아니며, 일부 내용을 선별하여 정리하였습니다.

Spring Security Architecture

📚 목차

  1. A Review of Filters
  2. DelegatingFilterProxy
  3. FilterChainProxy
  4. SecurityFilterChain
  5. Security Filters
  6. Printing the Security Filters
  7. Adding Filters to the Filter Chain
  8. Handling Security Exceptions
  9. Saving Requests Between Authentication
  10. Logging

1. A Review of Filters

FilterChain 개요

  • Spring Security의 보안 기능들은 Servlet Filter를 기반으로 동작한다.
  • Filter는 요청/응답을 가로채거나 수정할 수 있기 때문에, FilterChain의 구성과 각 필터의 실행 순서가 매우 중요하다.

2. DelegatingFilterProxy

📌 문제 상황: 필터 등록 시점 충돌

  1. 서블릿 컨테이너(Tomcat 등)는 애플리케이션 시작 시, 모든 Filter들을 미리 등록해야 함
  2. 하지만 우리가 쓰는 Spring Security Filter는 일반 Filter가 아닌 Spring Bean
  3. Spring BeanSpring Context가 초기화된 이후에야 생성됨 (ContextLoaderListener 이후)
  4. 따라서 컨테이너가 필터를 등록하려는 시점엔 아직 Spring Bean이 준비되지 않은 상태
    -> 즉, 필터가 필요한 시점생성 시점의 타이밍이 맞지 않는 문제 발생

✅ 해결 방법:

DelegatingFilterProxy를 통해 이 시점 차이를 해결할 수 있다.

  • 겉보기엔 일반 Filter처럼 등록됨 (서블릿 컨테이너가 인식할 수 있도록)
  • 실제 요청이 들어오면 -> 그때 Spring Context에서 해당 이름의 Filter Bean을 찾아서 실행함(지연 초기화 방식)

⭐️ 요약

  • DelegatingFilterProxy는 서블릿 컨테이너와 Spring Context의 초기화 시점 차이를 안전하게 연결해주는 브릿지 역할을 한다.

3. FilterChainProxy

  • 요청마다 어떤 SecurityFilterChain을 적용할지 결정하고 실행하는 중간 관리자 역할을 한다.

🧠 FilterChainProxy를 쓰는 이유

  1. Spring Security의 모든 필터 처리는 FilterChainProxy 하나로 시작됨
    • 문제 생기면 FilterChainProxy디버깅 포인트를 찍으면 모든 필터 동작을 추적할 수 있음.
  2. Spring Security 전용 기능을 다루기 위해 꼭 필요함
    • 예시:
      - 요청 처리 끝나고 SecurityContext(인증 정보)를 반드시 초기화해서 메모리 누수 방지
      - HttpFirewall을 통해 이상한 요청 차단 (ex. /..%2f..%2fadmin)
      -> 이런 것들은 일반 Filter에서는 처리 불가능. Spring Security의 철저한 보안 처리를 위해 꼭 필요함.
  3. URL 외의 조건으로도 필터 체인을 다르게 설정할 수 있음
    • 일반 Filter는 단순히 URL 패턴으로만 동작하지만 FilterChainProxy는 Spring의 RequestMatcher를 통해 더 복잡한 조건도 가능하다.
    • 예시:
      RequestMatcher matcher = new AndRequestMatcher(
      		    new AntPathRequestMatcher("/admin/**"),
      		    request -> request.getHeader("X-Secret-Key") != null
      		);

⭐️ 요약

  • FilterChainProxy는 Sprint Security의 핵심 필터 매니저 역할을 하며, 단순히 서블릿 필터를 위임하는 DelegatingFilterProxy보다 더 많은 역할을 수행한다.
  • 단순한 URL 패턴 매칭을 넘어, 유연한 요청 조건 처리보안 기능 적용을 위한 핵심 컴포넌트이다.

4. SecurityFilterChain

  • FilterChainProxy에 의해 현재 요청에 대하여 어떤 Spring Security Filter 인스턴스를 호출해야 하는지 결정한다.
  • 하나의 URL 조건에 대해 적용할 필터 목록을 정의함

📌 실무에서의 사용

  • 보통 SecurityFilterChain을 URL별로 나누는 방식으로 사용된다.
    - 요청 유형마다 요구하는 보안 정책이 다르기 때문
    - 체인을 분리하면 각 체인은 한 가지 역할만 담당하므로 깔끔하고 명확해짐

빈 SecurityFilterChain 설정

  • 어떤 SecurityFilterChain보안 필터를 하나도 포함하지 않을 수 있다.
    - 이는 특정 요청을 Spring Security가 무시하도록 설정하고 싶은 경우에 사용된다.

🆚 WebSecurityCustomizer vs 빈 SecurityFilterChain

구분WebSecurityCustomizer (web.ignoring())SecurityFilterChain 등록
목적Spring Security 자체를 완전히 우회Spring Security를 거치되, 아무 필터도 적용하지 않음
적용 시점FilterChainProxy 진입 전FilterChainProxy 진입 후, 필터가 없어서 바로 통과
보안 필터 동작아예 동작 안 함 (서블릿 필터 체인 자체를 안 탐)FilterChainProxy는 실행됨. 단, 필터가 없음
성능가장 빠름 (Spring Security 필터 아예 안 거침)약간 느림 (Security 진입은 하니까)
추천 사용처정적 리소스, 에러 페이지 등 전혀 보안이 필요 없는 요청요청은 보안 대상이지만, 명시적으로 허용해야 할 요청 (예: /public/**, 공개 API)
예시 요청/favicon.ico, /css/style.css/public/info, /docs/open-access

5. Security Filters

  • Spring Security는 여러 개의 보안 필터들을 정해진 순서대로 FilterChainProxy에 등록하여 실행한다.

🔢 필터 순서의 중요성

  • 필터는 올바른 순서대로 실행되어야 보안이 제대로 작동한다.
  • 예시:
    • 인증 필터가 먼저 실행되어야 인가 필터가 그 인증 결과를 기반으로 접근 권한을 판단한다.

🔍 필터 순서확인 방법

  • Spring Security의 내부 클래스인 FilterOrderRegistration를 통해 각 필터의 등록 순서를 확인할 수 있다.

⚠️ 커스텀 필터 추가시 주의사항

  • 필터를 추가할 때는 반드시 어느 필터 앞/뒤에 위치해야 하는지 명확히 알아야 한다.
  • 잘못된 순서에 배치하면 필터가 제대로 동작하지 않거나, 예상치 못한 예외가 발생할 수 있다.

📌 필터 선언 방식

  • 대부분의 보안 필터는 HttpSecurity DSL을 통해 선언된다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(Customizer.withDefaults())
            .httpBasic(Customizer.withDefaults())
            .formLogin(Customizer.withDefaults())
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            );

        return http.build();
    }

}

6. Printing the Security Filters

🔨 필터 디버깅 방법

  1. 애플리케이션 시작 시, 보안 필터 목록 확인하기
    • Spring Security는 애플리케이션이 실행될 때, 각 요청 경로(URL 패턴)마다 적용되는 보안 필터 목록을 DEBUG 로그 수준으로 출력
    • 이 로그를 통해 내가 추가한 필터가 정상적으로 필터 체인에 포함되었는지 확인 가능
  2. 요청마다 어떤 필터가 실제로 실행되는지 확인하기
    • 단순히 필터가 등록되었는지뿐 아니라, 특정 요청을 처리할 때 실제 어떤 필터들이 실행되었는지도 확인 가능

7. Adding Filters to the Filter Chain

➕ HttpSecurity에서 필터를 추가하는 3가지 방법

  1. #addFilterBefore(Filter, Class<?>) : 다른 필터 전에 필터를 추가함
  2. #addFilterAfter(Filter, Class<?>) : 다른 필터 후에 필터를 추가함
  3. #addFilterAt(Filter, Class<?>) : 다른 필터를 필터로 교체함
  • 커스텀 필터를 생성하는 경우, 필터 체인의 위치를 결정해야 한다.
  • Filter 인터페이스를 직접 구현하는 대신, 요청마다 딱 한 번만 실행되는 필터의 기본 클래스OncePerRequestFilter를 상속할 수 있다. (doFilterInternal 메서드 제공)

❌ Filter 중복 실행 방지

  • 필터를 Spring Bean으로 등록하면, Spring Boot는 자동으로 해당 필터를 서블릿 컨테이너에도 등록한다.
  • 그런데 Spring Security는 FilterChainProxy를 통해 이미 그 필터를 실행하고 있을 수 있기 때문에,
    필터가 한 요청에서 두 번 실행되는 문제가 발생할 수 있다.
  • 따라서 필터를 꼭 Spring Bean으로 등록해야 할 경우, FilterRegistrationBean을 사용해 enabled = false로 설정하여 서블릿 컨테이너에 중복 등록되지 않도록 해야 한다.
@Bean
public FilterRegistrationBean<MyCustomFilter> myFilterRegistration(MyCustomFilter filter) {
    FilterRegistrationBean<MyCustomFilter> registration = new FilterRegistrationBean<>(filter);
    registration.setEnabled(false); // ✅ 컨테이너에 등록되지 않도록 설정
    return registration;
}

🛠️ Customizing a Spring Security Filter

  • 필터의 DSL 메서드를 사용하여 스프링 보안 필터를 구성할 수 있다.
  • Spring Security는 HttpSecurity DSL을 통해 보안 필터를 자동으로 추가함.
  • 같은 필터를 addFilterAt()으로 직접 추가하려 하면 중복 등록으로 예외 발생함.
  • 이때는 기존 DSL 설정을 제거하거나 disable()로 비활성화해야 함.
.httpBasic((basic) -> basic.disable())

8. Handling Security Exceptions

📌 ExceptionTranslationFilter

  • Spring Security에서 인증 및 인가 과정에서 예외가 발생했을 때, 그 예외를 처리해주는 필터이다.
  • AccessDeniedException 및 AuthenticationException를 HTTP 응답으로 변환함

🔄 ExceptionTranslationFilter의 동작 흐름

try {
	filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
	if (!authenticated || ex instanceof AuthenticationException) {
		startAuthentication();
	} else {
		accessDenied();
	}
}
  1. 요청을 다음 필터로 넘김
    • ExceptionTranslationFilter는 요청을 그대로 다음 필터 체인에 전달
    • 예외가 발생하는 경우에만 동작함
  2. 예외가 발생한 경우
    • 2-1. 인증 예외 (AuthenticationException)
      - 사용자가 로그인하지 않았거나, 로그인에 실패한 경우
      - 처리 방식:
      - SecurityContextHolder비워서 이전 인증 정보 제거
      - 현재 요청(HttpServletRequest)을 RequestCache에 저장
      - AuthenticationEntryPoint를 호출 -> 로그인 페이지로 리다이렉트 또는 401 Unauthorized 응답
    • 2-2. 인가 예외 (AccessDeniedException)
      - 로그인은 했지만, 권한이 없는 리소스에 접근한 경우
      - 처리 방식:
      - AccessDeniedHandler 호출 -> 403 Forbidden 응답 반환
  3. 예외가 없을 경우
    • 아무 작업 없이 요청을 그대로 통과시킴

9. Saving Requests Between Authentication

  • 인증되지 않은 사용자가 인증이 필요한 자원에 접근하면, Spring Security는 해당 요청을 RequestCache에 저장해 둔다.
  • 이후 사용자가 로그인에 성공하면, 저장된 요청을 꺼내어 자동으로 다시 실행한다.
  • 이 덕분에 사용자는 로그인 후에도 원래 가려던 페이지로 자연스럽게 이동할 수 있다.

RequestCache: 원래 요청을 저장해두는 Spring Security의 인터페이스
HttpServletRequest: 사용자가 보낸 원래 요청 객체 (URL, 파라미터 포함)
리플레이(re-request): 로그인 성공 후, 저장된 요청을 다시 보내는 것

📦 RequestCache

  • ExceptionTranslationFilter
    - 인증 예외를 감지하면, 로그인 페이지로 리다이렉트하기 전에 요청을 저장
  • RequestCacheAwareFilter
    - 로그인에 성공한 뒤, RequestCache에 저장된 요청이 있는지 확인해서 다시 실행함
  • 기본 구현체는 HttpSessionRequestCache
    - 저장된 요청은 HTTP 세션(HttpSession) 에 보관되며, 로그인 이후에 해당 세션에서 요청 정보를 꺼내어 리다이렉트함.
  • 커스터마이징 가능
    - 예를 들어, 특정 파라미터가 있는 요청만 다시 실행하고 싶을 때
    - continue라는 파라미터가 있을 때만 RequestCache를 확인하도록 구현 가능
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
	HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
	requestCache.setMatchingRequestParameterName("continue");
	http
		// ...
		.requestCache((cache) -> cache
			.requestCache(requestCache)
		);
	return http.build();
}

🚫 Prevent the Request From Being Saved (요청 저장 비활성화)

  • Spring Security의 기본 동작은 인증되지 않은 사용자의 요청을 세션에 저장하는 것이지만, 필요에 따라 이 기능을 비활성화할 수 있다.

📌 요청을 저장하지 않으려는 이유

  • 세션을 사용하지 않으려는 경우
    • 요청 정보를 세션 대신 브라우저(쿠키 등)나 DB에 저장하고 싶을 때
  • 보안 또는 서버 부담을 줄이기 위해
    • 민감한 요청을 세션에 저장하지 않게 하고 싶을 때
  • 항상 고정된 페이지로 리다이렉트하고 싶은 경우
    • 로그인 후 원래 요청으로 돌아갈 필요 없이 홈(/) 등으로 이동시키고 싶을 때

해결 방법: NullRequestCache 

  • Spring Security가 제공하는 NullRequestCache는 이름 그대로, 요청을 전혀 저장하지 않는 구현체이다.
  • 이 구현체를 사용하면 로그인 후에도 이전 요청 정보가 없기 때문에, 항상 지정한 기본 페이지로 리다이렉트된다.
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
    RequestCache nullRequestCache = new NullRequestCache();
    http
        // ...
        .requestCache((cache) -> cache
            .requestCache(nullRequestCache)
        );
    return http.build();
}

10. Logging

  • Spring Security는 보안상 요청 거부 사유를 응답에 담지 않기 때문에, 디버깅을 위해 TRACE 수준 로그를 활성화하면 문제 원인을 로그에서 확인할 수 있다.
logging:
  level:
    org.springframework.security: TRACE

📝 마무리

Spring Security 공식 문서를 읽기 전에는 구조가 너무 복잡하고 어렵게 느껴졌으나 하나씩 따라가며 정리해보니 잘 몰랐던 동작 원리들을 조금씩 이해할 수 있었다. (물론 ChatGPT의 도움을 많이 받았다.😉)

이번 글이 Spring Security의 구조를 이해하는 데 조금이나마 도움이 되었길 바라며, 앞으로도 Spring Security에 대한 내용을 차근차근 더 정리해볼 예정이다. 🙇‍

profile
개발자로 성장하기

0개의 댓글