Spring Security 이해 (2) - 인증 아키텍처
이번엔 스프링 시큐리티의 서블릿 인증에서 사용되는 주요 아키텍처 구성 요소에 대해 살펴보자.
SecurityContextHolder
스프링 시큐리티에서 인증 모델의 핵심 요소이고, SecurityContext 를 포함하고 있다.
(SecurityContext 는 SecurityContextHolder 안에 들어있는 컨테이너로, 현재 요청의 Authentication 객체를 보관한다.)

인증된 사용자의 세부 정보를 저장하는 곳이고, 스프링 시큐리티는 SecurityContextHolder 가 내부적으로 어떻게 채워졌는지는 신경 쓰지는 않는다. 홀더에 들어있는 Authentication 을 현재 인증 정보로 사용하게 된다.
사용자 인증의 가장 간단한 방법은 그냥 이 홀더에 값을 설정해주면 된다.
SecurityContextHolder 설정하기:
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
- 먼저 비어있는
SecurityContext를 생성한다. 이 때, 여러 스레드 간의 경쟁 조건(race conditions)을 피하기 위해SecurityContextHolder.getContext().setAuthentication(authentication)대신 새로운SecurityContext인스턴스를 생성해야 한다. - 다음으로 새로운
Authentication객체를 생성한다. 스프링 시큐리티는 어떤 인증 객체가 설정되든 신경쓰지 않으므로 자유롭게 커스텀이 가능하다. - 마지막으로
SecurityContextHolder에SecurityContext를 설정해주면, 스프링 시큐리티는 이 정보를 가지고 인가(authorization)에 사용한다.
Q1: 왜 SecurityContextHolder.getContext() 를 사용하면 문제가 되지?
같은 세션을 공유하면서 동시 요청이 들어오는 웹 애플리케이션에서는, HttpSession 에 들어있는 동일한 SecurityContext 인스턴스가 여러 스레드에 의해 공유될 수 있다.
이 상태에서 SecurityContextHolder.getContext().setAuthentication() 처럼 그 인스턴스를 직접 변경(mutate) 하면, 다른 스레드 요청에도 영향이 갈 수 있다.
스레드가 실행되는 동안 컨텍스트를 일시적으로 바꾸는 코드가 없다면 문제는 없지만, 있다면 다른 스레드에 영향을 끼칠 수 있으므로 새로운 컨텍스트를 만들어 써야한다.
Q2: JWT를 사용할 때도 문제가 되는가?
보통 JWT를 사용하면, 세션 정책을 STATELESS 로 설정하게 된다. 그러면 요청 간 SecurityContext 를 로드/저장하지 않으므로(세션 영속화 X) 세션 공유로 인한 동일한 인스턴스 변경 문제가 줄어든다.
그런데 한 요청 안에서 스레드를 생성하는 경우엔 같은 컨텍스트를 공유하니, 수정을 할 경우 문제가 생길 수 있음에 유의한다.
인증된 주체(Principal)에 대한 정보를 얻고 싶다면, SecurityContextHolder 에 접근하면 된다.
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
SecurityContextHolder 가 컨텍스트를 저장하는 방식
- MODE_THREADLOCAL (기본값)
- 스레드마다 독립적으로
SecurityContext참조를 ThreadLocal에 보관한다. - 요청이 끝나면 clear된다.
- 요청 간 세션 저장이나 로드는
SecurityContextRepository이 수행한다.
- 스레드마다 독립적으로
- MODE_GLOBAL
- 정적 전역 변수 하나에 컨텍스트를 저장
- 모든 스레드가 동일한 하나의 컨텍스트 공유
- MODE_INHERITABLETHREADLOCAL
- 부모 스레드에 설정된 컨텍스트가 자식 스레드에 상속된다.
요청 생명주기에서의 SecurityContext 관리
요청이 시작되면 SecurityContextRepository가 저장소(예: HttpSession)에서 컨텍스트를 로드해 SecurityContextHolder에 설정한다. 요청이 끝난 후 컨텍스트를 저장소에 다시 저장할지는 사용하는 필터에 따라 달라진다.
SecurityContextPersistenceFilter(기존 방식, 6에서 deprecated): 요청 시작 시 로드 + 종료 시 자동 저장SecurityContextHolderFilter(6.x 이상): 로드만 수행하고 자동 저장하지 않음
SecurityContextHolderFilter 를 사용하는 환경에서 커스텀 인증 필터를 만들어 세션에 컨텍스트를 유지하고 싶다면, SecurityContextRepository#saveContext를 명시적으로 호출해야 한다.
기본 제공 인증 필터(AbstractAuthenticationProcessingFilter 등)는 내부적으로 저장을 처리해준다.
Authentication
Authentication 인터페이스는 스프링 시큐리티 안에서 두 개의 주요 목적이 있다.
- 유저가 인증을 위해 제공한 자격 증명(credentials)을
AuthenticationManager에 입력으로 제공. 이 때는 아직 인증 전이라isAuthenticated()메서드는false를 반환한다. - 현재 인증된 사용자를 나타낸다.
Authentication 에 다음과 같은 요소가 있다.
principal: 사용자를 식별한다. 사용자 이름/비밀번호로 인증할 때 주로UserDetails의 인스턴스가 된다.credentials: 주로 비밀번호다. 보통 사용자 인증이 되면 누출되지 않게 지운다.authorities: 사용자에게 부여된 상위 수준의 권한 목록들(각 목록은GrantedAuthority인스턴스)
AdditionalRequiredFactorsBuilder 를 이용해 Authentication 인스턴스를 변경하거나 다른 것과 병합할 수 있다.
Authentication latestResult = authenticationManager.authenticate(authenticationRequest);
Authentication previousResult = SecurityContextHolder.getContext().getAuthentication();
if (previousResult != null && previousResult.isAuthenticated()) {
latestResult = latestResult.toBuilder()
.authorities((a) -> a.addAll(previousResult.getAuthorities()))
.build();
}
폼 로그인 같은 인증 단계에서 권한을 가져온 다음, 일회용 토큰 인증 같은 다른 단계에서 권한을 추가하는 시나리오 같은 시나리오도 생각해볼 수 있다.
GrantedAuthority
사용자(principal)에게 부여된 상위 수준 권한(high-level permissions). 여기서 상위 수준이라 함은 애플리케이션/보안 정책에서의 관점이다. 역할(roles)과 스코프(scopes)가 있다. 나중에 인가를 위해 필요하다.
⇒ 일반적으로 GrantedAuthority 객체는 특정 도메인 객체에 대한 권한을 가지지 않는데, 그 이유는 만약 수천 개의 그런 권한이 있으면 메모리를 많이 잡아먹거나 사용자를 인증하는 데 오랜 시간이 걸린다.
Authentication.getAuthorities()메서드로GrantedAuthority객체의 컬렉션을 얻을 수 있다.- username/password 기반 인증을 사용할 때 보통
GrantedAuthority인스턴스는 보통UserDetailsService에 의해 로드된다.
AuthenticationManager
스프링 시큐리티 필터가 인증을 수행(성공/실패 결정)할 수 있도록 API를 제공한다.
@FunctionalInterface
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
시큐리티 필터를 사용하는 경우, 필터가 AuthenticationManager 를 호출하고 결과를 SecurityContextHolder 에 넣는 것까지 자동으로 처리한다.
커스텀으로 필터를 만들고 시큐리티 필터 인스턴스와 통합하지 않으면, 굳이 AuthenticationManager 를 거칠 필요 없고 바로 컨텍스트 홀더에 인증을 넣어주면 된다.
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(myCustomAuthentication);
SecurityContextHolder.setContext(context);
ProviderManager
가장 흔히 사용되는 AuthenticationManager 의 구현체다. 이 ProviderManager는 인증 판정에 대한 총괄 관리자고, 인증 자체는 Provider들에게 위임한다.
⇒ 내부에 List<AuthenticationProvider> 를 가지고 있고, 리스트를 순회하면서 순서대로 Provider에게 인증을 처리할 수 있는지 물어본다.

각 AuthenticationProvider 들은 각자 맡은 인증 방법의 유형이 다르다.
- Username/Password 검증 (DaoAuthenticationProvider)
- JWT 검증 (JwtAuthenticationProvider)
- OAuth 검증 등…
Provider는 다음 3가지 중 하나를 한다.
- 인증 성공 →
Authentication반환 - 인증 실패 →
AuthenticationException에러 - 판단 불가 (supports = false)
만약에 리스트에 있는 어떠한 Provider로도 인증할 수 없으면, ProviderNotFoundException 에러와 함께 인증에 실패하게 된다.

매니저가 부모를 가질 수도 있는데, 만약 어떠한 Provider로도 인증할 수 없을 때 부모가 있다면 참조해 인증을 시도할 수 있다.

여러 매니저들이 동일한 부모를 참조할 수도 있다. (여러 SecurityFilterChain 인스턴스가 있는 경우)
기본적으로 ProviderManager 는 인증 요청이 성공해 반환된 Authentication 객체에서 민감한 자격 증명 정보(credentials)를 지우려고 한다. ⇒ 비밀번호 같은 정보가 세션에 오랫동안 보관되는 것을 방지한다.
Authentication객체가CredentialsContainer이면eraseCredentials()메서드를 호출해 민감한 정보들을 지워버린다.- 지우고 싶지 않는 경우가 있을 수 있는데, 그 때는 매니저의
eraseCredentialsAfterAuthentication속성을false로 바꾸면 된다.
AuthenticationEntryPoint
클라이언트에게 자격 증명을 요청하는 HTTP 응답을 보내는 데 사용된다. (인증이 필요한데 인증이 안된 경우)
⇒ 접근 권한이 없는 리소스에 인증되지 않은 요청을 보내면
- 로그인 페이지로 리다이렉트하거나
WWW-Authenticate헤더로 응답한다.- 다른 작업을 할 수도 있다.
AbstractAuthenticationProcessingFilter
주로 세션 기반 인증에서, 사용자의 자격 증명을 인증하기 위한 기본 필터로 사용된다.

(1) 사용자가 자격 증명을 제출하면, 필터가 인증 객체를 만들게 된다.
- 필터에 따라 인증 객체 유형은 달라질 수 있다.
UsernamePasswordAuthenticationFilter는 HttpServletRequest에 제출된 사용자 이름과 비밀번호로UsernamePasswordAuthenticationToken을 생성한다.
(2) AuthenticationManager 에 인증을 위해 생성한 인증 객체를 전달한다.
(3) 인증에 실패하면:
SecurityContextHolderclearRememberMeServices.loginFail호출. 단, remember me가 설정되지 않았다면 아무것도 안한다.(no-op)AuthenticationFailureHandler호출
(4) 인증에 성공하면:
SessionAuthenticationStrategy에 로그인이 됐다고 알린다.SecurityContextHolder에 이미 인증된Authentication이 있으면 여기에 있던 권한들을 (2)에서 방금 인증된Authentication객체에 추가한다.SecurityContextHolder에 인증된 인증 객체를 설정한다. (시큐리티가 제공하는 인증 필터들은 내부적으로 저장 로직이 있으므로, 직접 커스텀 필터를 만들 때만 신경 쓰면 된다.)RememberMeServices.loginSuccess호출. 단, remember me가 설정되지 않았다면 no-opApplicationEventPublisher가InteractiveAuthenticationSuccessEvent이벤트 발행AuthenticationSuccessHandler호출
레퍼런스
Spring Security 7.0.2 - Servlet Authentication Architecture
https://vcfvct.wordpress.com/2014/09/16/thread-safe-in-spring-security/
'개발 > Backend' 카테고리의 다른 글
| 무신사 코딩테스트 결과물 개선 및 부하테스트 해보기 (5) | 2026.02.21 |
|---|---|
| Spring Security 이해 (1) - 서블릿 기반 아키텍처 (0) | 2026.02.10 |
| [Spring Boot/JPA] Hibernate Naming Strategy (0) | 2026.01.20 |
| [Spring Boot] DataSource & HikariCP 설정 정리 (0) | 2026.01.10 |