Spring Security Architecture

Version boundary

이 페이지는 Servlet filter delegation architecture를 설명한다. Filter order와 security configuration DSL은 Spring Security version에 따라 달라질 수 있으므로, 개별 설정 문법보다 책임 경계를 우선한다.

Spring Security는 Servlet Filter Chain 기반으로 동작한다. 모든 HTTP 요청이 Spring MVC Controller의 DispatcherServlet에 도달하기 전에, Filter 단에서 인증·인가를 수행한다. Spring AOP와 비교하면 Filter(보안) vs Interceptor(MVC) vs AOP(Bean 메서드)는 요청을 가로채는 위치가 서로 다르다.

Filter가 가장 바깥에서 요청을 가로채는 이유

Spring Security의 Servlet 지원 기능은 Servlet Filter를 기반으로 구현된다.[1] 클라이언트가 요청을 보내면 Servlet Container는 요청 URI 경로를 기준으로 FilterChain을 생성하고, FilterChain에는 Filter 목록과 최종 처리할 Servlet(보통 DispatcherServlet)이 포함된다.[1:1]

flowchart TD
  Client["Client 요청"]
  FC["FilterChain
(Spring Security)"] AuthFilter["AuthenticationFilter
→ 인증"] AuthzFilter["AuthorizationFilter
→ 인가"] DS["DispatcherServlet
(Spring MVC)"] Interceptor["HandlerInterceptor
(선택적)"] Ctrl["Controller"] Client --> FC FC --> AuthFilter --> AuthzFilter AuthzFilter -->|인증·인가 통과| DS AuthzFilter -->|거부| Client DS --> Interceptor --> Ctrl

Filter는 이후 Filter 또는 Servlet 실행을 막고 직접 응답을 작성할 수 있다(예: 인증되지 않은 사용자 차단).[2] 보안은 가능한 한 가장 바깥 계층에서 처리해야 하며, 인증되지 않은 요청은 DispatcherServlet까지 도달하지 못하고 Filter 단계에서 차단된다.[3]

Filter vs Interceptor vs AOP

구분 위치 가로채는 대상 Spring Bean 접근
Filter DispatcherServlet 앞 HTTP 요청 자체 DelegatingFilterProxy로 간접
Interceptor DispatcherServlet 뒤 Controller 호출 ApplicationContext 직접 접근
AOP Bean 메서드 호출 Service/Repository 메서드 Proxy 기반

Spring Security는 Servlet Filter 계층에서 동작하며, Interceptor는 DispatcherServlet 이후에 실행되므로 위치가 다르다.[3:1] Interceptor는 AOP의 일종이 아니다 — 둘 다 횡단 관심사 분리라는 공통 목적이 있지만, AOP는 프록시 기반이고 Interceptor는 Spring MVC 요청 처리 파이프라인 내부에서 동작한다.[4] Filter, Interceptor, AOP는 모두 공통 로직을 비즈니스 로직과 분리하는 도구이지만 가로채는 위치가 다르며, Filter → Interceptor → AOP 순으로 애플리케이션 내부 깊숙이 들어간다.[5]

Filter 순서(Order)는 자신 이후에 위치한 Filter와 Servlet에만 영향을 줄 수 있으므로 매우 중요하다. AuthenticationFilter가 AuthorizationFilter보다 먼저 실행되어야 권한 검사 필터가 현재 사용자를 확인할 수 있다.[6]

DelegatingFilterProxy — Servlet Container와 Spring Bean의 브릿지

Servlet Container는 Spring Bean을 직접 알지 못한다.[7] DelegatingFilterProxy는 이 간극을 메우는 중간 브릿지로, Spring ApplicationContext에서 Filter Bean을 찾아 실제 작업을 위임한다.[7:1]

flowchart LR
  SC["Servlet Container
(Spring Bean 모름)"] DFP["DelegatingFilterProxy
(브릿지)"] Bean["Spring Bean Filter
(ApplicationContext)"] SC --> DFP --> Bean
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
    Filter delegate = getFilterBean(someBeanName);  // Spring Bean 조회
    delegate.doFilter(request, response);            // 위임
}

FilterChainProxy와 SecurityFilterChain

인증 실패 응답: ExceptionTranslationFilter와 authenticationEntryPoint

flowchart LR
  Req["요청
(토큰 없음·만료)"] --> Jwt["JwtAuthenticationFilter
익명으로 통과"] Jwt --> Authz["AuthorizationFilter
authenticated() 경로면 거부"] Authz --> ETF["ExceptionTranslationFilter"] ETF --> EP["authenticationEntryPoint"] EP -->|미설정| R403["빈 403"] EP -->|설정| R401["401 + JSON"]

필터에서 JSON을 직접 써야 하는 이유

.exceptionHandling(ex -> ex.authenticationEntryPoint(
        (request, response, authException) -> writeError(response, ErrorCode.LOGIN_REQUIRED)))

private void writeError(HttpServletResponse response, ErrorCode errorCode) throws IOException {
    response.setStatus(errorCode.getStatus());
    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    response.setCharacterEncoding(StandardCharsets.UTF_8.name()); // 한글 메시지 깨짐 방지
    jsonMapper.writeValue(response.getWriter(),
            new ErrorResponseDto(errorCode.getCode(), errorCode.getMessage()));
}

실패 모드

관련

출처

테스트 질문


  1. Spring Security 아키텍처.md — "Spring Security의 Servlet 지원 기능은 Servlet Filter를 기반으로 구현되어 있습니다.", "클라이언트가 애플리케이션에 요청을 보내면 Servlet Container는 요청 URI 경로를 기준으로 해당 요청을 처리할 FilterChain을 생성합니다." ↩︎ ↩︎

  2. Spring Security 아키텍처.md — "이 경우 Filter가 직접 응답을 작성합니다.", "예를 들어 인증되지 않은 사용자의 접근을 차단하는 경우가 이에 해당합니다." ↩︎

  3. Spring Security 아키텍처.md — "Spring Security는 Servlet Filter 계층에서 동작한다.", "보안은 가능한 한 가장 바깥 계층에서 처리해야 한다.", "인증되지 않은 요청은 DispatcherServlet까지 도달하지 못하고 Filter 단계에서 차단된다." ↩︎ ↩︎

  4. Spring Security 아키텍처.md — "둘 다 횡단 관심사(Cross-Cutting Concern)를 분리한다는 공통 목적은 있지만 구현 방식이 다르다.", "AOP는 프록시 기반이고, Interceptor는 Spring MVC 요청 처리 파이프라인 내부에서 동작한다." ↩︎

  5. Spring Security 아키텍처.md — "셋 모두 공통 로직을 핵심 비즈니스 로직과 분리하기 위한 도구다.", "Filter ↓ Interceptor ↓ AOP 순으로 애플리케이션 내부 깊숙한 곳으로 들어간다." ↩︎

  6. Spring Security 아키텍처.md — "Filter는 자신 이후에 위치한 Filter와 Servlet에만 영향을 줄 수 있다.", "인증 필터가 먼저 실행되어야 권한 검사 필터가 현재 사용자를 확인할 수 있다." ↩︎

  7. Spring Security 아키텍처.md — "Servlet Container는 Spring Bean을 직접 알지 못하기 때문에 Spring Security는 중간 브릿지 역할을 하는 DelegatingFilterProxy를 사용한다.", "DelegatingFilterProxy는 Spring ApplicationContext에서 Filter Bean을 찾아 실제 작업을 위임한다." ↩︎ ↩︎

  8. spring-security-servlet-architecture.md — "Spring Security's Servlet support is contained within FilterChainProxy.", "FilterChainProxy is a special Filter provided by Spring Security that allows delegating to many Filter instances through SecurityFilterChain." ↩︎

  9. spring-security-servlet-architecture.md — "Only the first SecurityFilterChain that matches is invoked. If a URL of /api/messages/ is requested, it first matches on the SecurityFilterChain0 pattern of /api/**, so only SecurityFilterChain0 is invoked, even though it also matches on SecurityFilterChainn.", "each SecurityFilterChain can be unique and can be configured in isolation."(L118), "a SecurityFilterChain might have zero security Filter instances if the application wants Spring Security to ignore certain requests." ↩︎ ↩︎

  10. 출처 매핑 미확인 — "어떤 chain에도 매칭되지 않은 요청은 다음 filter로 진행하며 이는 permitAll과 다르다"는 구분은 이 페이지가 인용하는 두 소스 어디에도 grep되지 않는다. Spring Security 공식 문서의 RequestMatcher/ignoring() 관련 절을 추가로 확인해야 한다. ↩︎

  11. 인증 실패 응답·좋아요 중복 방지 노트 — "응답이 본문 없는 403이었다.", "그래서 프론트가 "로그인 만료"와 "권한 없음(남의 글)"을 구분할 수 없었다.", "authenticationEntryPoint를 정해서 401 + {"code":4011,"message":"Login required."}를 주게 했다.", "누가 어디에 접근할 수 있는지(requestMatchers)는 바꾸지 않았다. 바꾼 건 막힐 때의 응답 모양뿐이다." ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  12. 인증 실패 응답·좋아요 중복 방지 노트 — "JwtAuthenticationFilter: 토큰이 이상해도 막지 않고 "익명"으로 넘긴다.", "AuthorizationFilter: authenticated() 경로인데 익명이면 거부한다.", "ExceptionTranslationFilter: 익명 사용자의 거부를 받아 authenticationEntryPoint를 호출한다. 따로 정해 두지 않으면 기본값은 빈 403이다." ↩︎

  13. 인증 실패 응답·좋아요 중복 방지 노트 — "Security 필터도 스프링 빈이라 컨테이너 안에 있다. 막히는 지점은 컨테이너 밖이 아니라 DispatcherServlet 앞이다.", "HttpMessageConverter가 Jackson으로 객체를 JSON으로 바꾸고 상태 코드와 Content-Type을 맞춘다.", "@RestControllerAdvice가 예외를 잡는다.", "필터 단계에서는 둘 다 아직 동작하지 않는다. 그래서 Jackson(JsonMapper)을 직접 호출해 응답을 쓴다." ↩︎ ↩︎ ↩︎ ↩︎

  14. 인증 실패 응답·좋아요 중복 방지 노트 — writeError 코드("한글 메시지 깨짐 방지"), "JsonMapper는 new로 만들지 않고 주입받는다. Boot가 만든 것을 써야 컨트롤러 응답과 같은 규칙으로 JSON이 나온다.", "Boot 4는 Jackson 3이라 패키지가 tools.jackson이다." ↩︎ ↩︎ ↩︎