Spring Security 6.x - SecurityContextHolder 동작 원리 완벽 정리

존스노우·2025년 11월 27일

springSecurity

목록 보기
77/77
# Spring Security 6.x - SecurityContextHolder 동작 원리 완벽 정리

## 🤔 왜 JWT 인증할 때 매번 setAuthentication()을 호출할까?

JWT 기반 인증 코드를 보면 항상 이런 패턴이 있다.

```java
SecurityContextHolder.getContext().setAuthentication(authentication);

왜 매 요청마다 이걸 해줘야 할까? 공식 문서를 기반으로 정리해본다.


📌 핵심 개념: SecurityContextHolder

Spring Security는 SecurityContextHolder가 어떻게 채워지는지 신경 쓰지 않는다. 값이 포함되어 있으면, 그것이 현재 인증된 사용자로 사용된다.
Spring Security 공식 문서

SecurityContextHolder 구조

SecurityContextHolder
    └── SecurityContext
            └── Authentication (현재 인증된 사용자 정보)

📌 ThreadLocal 기반 저장

공식 문서 내용

기본적으로 SecurityContextHolder는 ThreadLocal 객체를 사용하여 보안 컨텍스트를 저장한다. 이는 같은 스레드 내의 메서드들에서는 SecurityContext 객체를 직접 전달하지 않아도 항상 보안 컨텍스트에 접근할 수 있다는 것을 의미한다.

쉬운 비유: 사무실 책상

[Thread-1의 책상]          [Thread-2의 책상]          [Thread-3의 책상]
   - 김철수 정보              - 이영희 정보              - 박민수 정보
  • 각 스레드는 자기 ThreadLocal(책상)만 볼 수 있음
  • 다른 스레드의 정보는 볼 수 없음
  • 요청이 끝나면 책상이 치워짐 (clear)

📌 Stateless REST API (JWT)에서 매번 set 하는 이유

공식 문서 내용

stateless RESTful 웹 서비스와 같은 많은 다른 유형의 애플리케이션은 HTTP 세션을 사용하지 않고 매 요청마다 재인증한다.
Spring Security Technical Overview

흐름도

=== 요청 1: 김철수가 API 호출 ===

[Thread-1]
    │
    ▼
┌─────────────────────────────────┐
│  JWT 필터                        │
│  1. JWT에서 "김철수" 정보 추출    │
│  2. setAuthentication(김철수)    │  ← set!
└─────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────┐
│  Controller                      │
│  getAuthentication() → "김철수"  │
└─────────────────────────────────┘
    │
    ▼
[요청 끝 → SecurityContext 비워짐]


=== 요청 2: 이영희가 API 호출 ===

[Thread-1]  ← 같은 스레드 재사용 가능 (Thread Pool)
    │
    ▼
┌─────────────────────────────────┐
│  JWT 필터                        │
│  1. JWT에서 "이영희" 정보 추출    │
│  2. setAuthentication(이영희)    │  ← 다시 set!
└─────────────────────────────────┘

Thread Pool 방식이라 같은 스레드가 재사용될 수 있지만, 요청 끝날 때 clear 되므로 매번 새로 set 해야 한다!


📌 누가 SecurityContext를 Clear 하는가?

Spring Security 5.x vs 6.x

버전LoadSaveClear
5.xSecurityContextPersistenceFilterSecurityContextPersistenceFilterSecurityContextPersistenceFilter
6.xSecurityContextHolderFilter명시적 호출 필요FilterChainProxy

공식 문서 내용

FilterChainProxy는 Spring Security 사용의 핵심이기 때문에, 선택사항으로 보이지 않는 작업들을 수행할 수 있다. 예를 들어, 메모리 누수를 방지하기 위해 SecurityContext를 clear한다.
Spring Security Architecture

FilterChainProxy 소스 코드

// FilterChainProxy.java
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
    if (clearContext) {
        try {
            request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
            doFilterInternal(request, response, chain);
        } finally {
            securityContextHolderStrategy.clearContext();  // ← 여기서 clear!
            request.removeAttribute(FILTER_APPLIED);
        }
    } else {
        doFilterInternal(request, response, chain);
    }
}

finally 블록이므로 예외가 발생하든 안 하든 무조건 실행된다!


📌 왜 Clear 해야 하는가?

공식 문서 내용

컨텍스트가 저장된 ThreadLocal을 clearing하는 것은 필수적이다. 그렇지 않으면 스레드가 특정 사용자의 보안 컨텍스트가 여전히 붙어있는 상태로 서블릿 컨테이너의 스레드 풀에 반환될 수 있다. 이 스레드는 나중에 사용되어 잘못된 자격 증명으로 작업을 수행할 수 있다.
Spring Security Core Web Filters

즉, 보안 사고 방지를 위해 반드시 clear 해야 한다!


📌 Strategy 패턴 적용

SecurityContextHolderStrategy

SecurityContextHolder는 Strategy 패턴을 사용한다.

// SecurityContextHolder.java
public static void clearContext() {
    strategy.clearContext();  // 전략에 위임!
}

기본 전략 설정 코드

// SecurityContextHolder.java
private static void initializeStrategy() {
    if (!StringUtils.hasText(strategyName)) {
        strategyName = "MODE_THREADLOCAL";  // ← 기본값!
    }
    // strategyName에 따라 구현체 생성...
}

전략 종류

모드구현체설명
MODE_THREADLOCALThreadLocalSecurityContextHolderStrategy기본값, 스레드별 저장
MODE_INHERITABLETHREADLOCALInheritableThreadLocalSecurityContextHolderStrategy자식 스레드에 상속
MODE_GLOBALGlobalSecurityContextHolderStrategy전역 저장 (서버 비권장)

📌 전체 흐름 정리

요청 시작
    │
    ▼
FilterChainProxy.doFilter()
    │
    ├── try {
    │       │
    │       ▼
    │   SecurityContextHolderFilter
    │       → SecurityContext 로드
    │       │
    │       ▼
    │   JWT Filter
    │       → 토큰 검증
    │       → setAuthentication()
    │       │
    │       ▼
    │   Controller 처리
    │   }
    │
    └── finally {
            securityContextHolderStrategy.clearContext()
                    │
                    ▼
            ThreadLocalSecurityContextHolderStrategy.clearContext()
                    │
                    ▼
            ThreadLocal.remove()  ← 실제 제거!
        }
    │
    ▼
응답 반환

✅ 핵심 요약

  1. SecurityContextHolder는 기본적으로 ThreadLocal에 인증 정보를 저장한다.

  2. JWT(Stateless) 환경에서는 세션을 사용하지 않으므로 매 요청마다 setAuthentication() 해야 한다.

  3. 요청이 끝나면 FilterChainProxyfinally 블록에서 자동으로 clear된다.

  4. Clear 하지 않으면 Thread Pool에서 스레드 재사용 시 이전 사용자 정보가 남아있을 수 있다 (보안 사고!)

  5. SecurityContextHolder는 Strategy 패턴을 사용하며, 기본 전략은 MODE_THREADLOCAL이다.


📚 참고 자료

profile
어제의 나보다 한걸음 더

0개의 댓글