- Spring Security 의 세션 관리 필터들의 역할과 동작 원리
- SecurityContextHolderFilter, SessionManagementFilter, ConcurrentSessionFilter 의 구조, 동작 과정
- 각 필터가 SecurityContext, HttpSession, 세션 인증 전략과 어떻게 연결되는가
- 필터 체인에서의 실행 순서를 다이어그램으로 익히기
- 실무에서 세션 고정 공격 방어, 동시 세션 제어를 어떻게 적용하는가
세션 관리 필터 개요
Spring Security는 필터 기반 아키텍처로 동작한다
인증과 인가뿐만 아니라, 세션 관리 역시 여러개의 필터를 통해 처리된다
세션과 관련된 대표적인 필터들
| 필터 | 주요 역할 |
| SecurityContextHolderFilter | 요청마다 SecurityContext 생성 및 복원 |
| SessionManagementFilter | 인증시 세션 전략 적용 (세션 고정 보호, 동시 세션 제어 등) |
| ConcurrentSessionFilter | 이미 존재하는 세션의 유효성 검사 및 만료 처리 |
SecurityContextHolderFilter
여기에 있다
package org.springframework.security.web.context;
1. 역할과 동작 원리
- SecurityContextHolderFilter 는 요청이 들어올 때마다 SecurityContext 를 생성하거나
HttpSession 에서 기존의 Context를 복원한다 - 요청 처리 후에는 ThreadLocal 에 저장된 Context를 제거한다
SecurityContextHolderFilter 전문
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.function.Supplier;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;
public class SecurityContextHolderFilter extends GenericFilterBean {
private static final String FILTER_APPLIED = SecurityContextHolderFilter.class.getName() + ".APPLIED";
private final SecurityContextRepository securityContextRepository;
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) {
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
this.securityContextRepository = securityContextRepository;
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
} else {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
//이 부분에서 SecurityContext를 가져온다 (세션에서 복원)
Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
try {
this.securityContextHolderStrategy.setDeferredContext(deferredContext);
chain.doFilter(request, response);
} finally {
// 요청 처리 후 ThreadLocal 에 저장된 Context 제거
this.securityContextHolderStrategy.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
}
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
}
이 부분에서 SecurityContext 를 가져오거나 세션에서 기존 Context 를 복원한다
//이 부분에서 SecurityContext를 가져온다 (세션에서 복원)
Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
try {
this.securityContextHolderStrategy.setDeferredContext(deferredContext);
chain.doFilter(request, response);
그리고 요청 처리 후에는 아래 로직을 통해 ThreadLocal 에 저장된 Context 를 제거함
} finally {
// 요청 처리 후 ThreadLocal 에 저장된 Context 제거
this.securityContextHolderStrategy.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
2. HttpSessionSecurityContextRepository
- 기본적인 구현체로, HttpSession 에 SecurityContext 를 저장 / 조회한다
- 로그인 성공시 인증 객체(Authentication) 를 담은 SecurityContext 가 세션에 저장된다
이곳에 있음
package org.springframework.security.web.context;
HttpSessionSecurityContextRepository 의 일부
public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
protected final Log logger = LogFactory.getLog(this.getClass());
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
private Object contextObject;
private boolean allowSessionCreation;
private boolean disableUrlRewriting;
private String springSecurityContextKey;
private AuthenticationTrustResolver trustResolver;
public void saveContext(SecurityContext context,
HttpServletRequest request,
HttpServletResponse response) {
SaveContextOnUpdateOrErrorResponseWrapper responseWrapper =
(SaveContextOnUpdateOrErrorResponseWrapper)WebUtils
.getNativeResponse(response, SaveContextOnUpdateOrErrorResponseWrapper.class);
if (responseWrapper == null) {
this.saveContextInHttpSession(context, request);
} else {
responseWrapper.saveContext(context);
}
}
private void saveContextInHttpSession(SecurityContext context, HttpServletRequest request) {
if (!this.isTransient(context) && !this.isTransient(context.getAuthentication())) {
SecurityContext emptyContext = this.generateNewContext();
if (emptyContext.equals(context)) {
HttpSession session = request.getSession(false);
this.removeContextFromSession(context, session);
} else {
boolean createSession = this.allowSessionCreation;
HttpSession session = request.getSession(createSession);
this.setContextInSession(context, session);
}
}
}
private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
if (httpSession == null) {
this.logger.trace("No HttpSession currently exists");
return null;
} else {
Object contextFromSession = httpSession.getAttribute(this.springSecurityContextKey);
if (contextFromSession == null) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Did not find SecurityContext in HttpSession %s using the SPRING_SECURITY_CONTEXT session attribute", httpSession.getId()));
}
return null;
} else if (!(contextFromSession instanceof SecurityContext)) {
this.logger.warn(LogMessage.format("%s did not contain a SecurityContext but contained: '%s'; are you improperly modifying the HttpSession directly (you should always use SecurityContextHolder) or using the HttpSession attribute reserved for this class?", this.springSecurityContextKey, contextFromSession));
return null;
} else {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Retrieved %s from %s", contextFromSession, this.springSecurityContextKey));
} else if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Retrieved %s", contextFromSession));
}
return (SecurityContext)contextFromSession;
}
}
}
}
좀 길긴한데 원문을 볼 가치는 있다
팁
- SecurityContextPersistenceFilter 대신 SecurityContextHolderFilter 가 최신 버전(Security 6.x) 에서 사용된다
- 커스텀 저장소를 원한다면, SecurityContextRepository 를 구현할수도 있다 (Redis에 저장한다던지..)
SessionManagementFilter
1. 역할
- 로그인 시점에 동작하여 세션 인증 전략(SessionAuthenticationStrategy) 을 실행한다
- 주로 세션 고정 보호, 동시 세션 제어를 처리한다
여기있다
package org.springframework.security.web.session;
public class SessionManagementFilter extends GenericFilterBean {
static final String FILTER_APPLIED = "__spring_security_session_mgmt_filter_applied";
private SecurityContextHolderStrategy securityContextHolderStrategy;
private final SecurityContextRepository securityContextRepository;
private SessionAuthenticationStrategy sessionAuthenticationStrategy;
private AuthenticationTrustResolver trustResolver;
private InvalidSessionStrategy invalidSessionStrategy;
private AuthenticationFailureHandler failureHandler;
private void doFilter(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (request.getAttribute("__spring_security_session_mgmt_filter_applied") != null) {
chain.doFilter(request, response); // 체인 진행
} else {
request.setAttribute("__spring_security_session_mgmt_filter_applied", Boolean.TRUE);
if (!this.securityContextRepository.containsContext(request)) {
Authentication authentication =
this.securityContextHolderStrategy.getContext().getAuthentication();
if (this.trustResolver.isAuthenticated(authentication)) {
try {
this.sessionAuthenticationStrategy
.onAuthentication(authentication, request, response);
} catch (SessionAuthenticationException var6) {
SessionAuthenticationException ex = var6;
this.logger.debug("SessionAuthenticationStrategy rejected the authentication object", ex);
this.securityContextHolderStrategy.clearContext();
this.failureHandler.onAuthenticationFailure(request, response, ex);
return;
}
this.securityContextRepository.saveContext(
this.securityContextHolderStrategy.getContext(), request, response);
} else if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) {
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Request requested invalid session id %s", request.getRequestedSessionId()));
}
if (this.invalidSessionStrategy != null) {
this.invalidSessionStrategy.onInvalidSessionDetected(request, response);
return;
}
}
}
chain.doFilter(request, response); // 체인 진행
}
}
}
2. 세션 고정(Session Fixation) 보호
- 공격자가 미리 발급받은 세션ID를 피해자에게 심어주고, 로그인 후에도 동일한 세션을 공유하는 공격
- Security는 기본적으로 로그인 시 세션 ID를 변경하여 보호한다
http
.securityMatcher("/me/**")
.sessionManagement(session -> session
.sessionFixation(SessionManagementConfigurer.SessionFixationConfigurer::migrateSession)
)
3. 동시 세션 제어하기
한 사용자가 동시에 여러개의 세션을 가질 수 없도록 제한한다
http
.securityMatcher("/me/**")
.sessionManagement(session -> session
.maximumSessions(1) // 최대 1개의 세션만 허용하기
.expiredUrl("/session-expired")
)
팁
- 금융 서비스나 보안에 민감한 서비스에서는 동시 로그인 제한을 반드시 적용해야한다!
- 세션 고정 보호는 기본적으로 활성화 된 상태이나, 구체적인 전략(migrateSession, newSession) 을 검토하는게 좋다
ConcurrentSessionFilter
1. 역할
- 요청이 들어올때마다 세션의 유효성을 검사한다
- 세션이 만료되었거나 허용 갯수를 초과한 경우, 사용자를 강제로 로그아웃 처리한다
여기있음
package org.springframework.security.web.session;
ConcurrentSessionFilter 의 일부
public class ConcurrentSessionFilter extends GenericFilterBean {
private SecurityContextHolderStrategy securityContextHolderStrategy =
SecurityContextHolder.getContextHolderStrategy();
private final SessionRegistry sessionRegistry;
private void doFilter(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpSession session = request.getSession(false);
if (session != null) { // 필터링1
// info
SessionInformation info = this.sessionRegistry.getSessionInformation(session.getId());
if (info != null) { // info에 대한 필터링1
if (info.isExpired()) { // 만료됐으면
this.logger.debug(LogMessage.of(() -> {
return "Requested session ID " +
request.getRequestedSessionId() + " has expired.";
}));
this.doLogout(request, response); // 로그아웃 처리
this.sessionInformationExpiredStrategy.onExpiredSessionDetected(
new SessionInformationExpiredEvent(info, request, response, chain));
return;
}
// 만료되지 않았다면, refresh한다
this.sessionRegistry.refreshLastRequest(info.getSessionId());
}
}
// 필터 진행
chain.doFilter(request, response);
}
}
팁
- SessionRegistry 를 통해 현재 로그인된 모든 사용자 세션을 관리할 수 있다
- 관리자 페이지에서 강제 로그아웃 기능을 구현할 때 유용하다. 당신은 로그아웃됐어요!
정리
| 필터 | 주요 역할 | 실무 적용 |
| SecurityContextHolderFilter | 요청마다 SecurityContext 생성 및 복원 | 세션 저장소 커스터마이징 가능 (예 : Redis) |
| SessionManagementFilter | 로그인시 세션 전략을 적용함 | 세션 고정 방지, 동시 세션 제어 |
| ConcurrentSessionFilter | 요청마다 세션 만료 여부를 확인함 | 강제 로그아웃, 관리자 제어 |
'Spring Boot > Security 쿠키,세션 기반 인증,인가' 카테고리의 다른 글
| 세션 관리 설정과 커스터마이징 : 세션 만료 후 처리 전략 (0) | 2026.05.08 |
|---|---|
| 세션 관리 설정과 커스터마이징 : 세션 타임아웃 설정 (0) | 2026.05.08 |
| 세션 관리 설정, 커스터마이징 : 세션 생성 정책 설정 (0) | 2026.05.07 |
| 세션 관리 핵심 컴포넌트 : 세션 이벤트와 리스너 (0) | 2026.05.07 |
| 세션 관리 핵심 컴포넌트 : SecurityContextHolder 와 SecurityContext (0) | 2026.05.06 |