[개발지식] Web Application의 GateKeeper #9 - Session Strategy 분석

Hyo Kyun Lee·2025년 11월 30일

개발지식

목록 보기
110/131

1. 개요

인증처리를 진행하였더라도 이를 유지하지 못한다면, 로그인을 했어도 다른 페이지에 들어갈때마다 혹은 요청을 할때마다 로그인을 계속해야 하는 모순적인 상황이 발생한다.

이러한 번거로움을 막고 사용자의 인증유지를 할 수 있는 가장 기본이 바로 session strategy, 세션관리정책이다.

세션을 어떻게 관리하고 Spring Security에서는 이를 어떠한 방식으로 지원하는지 그 과정에 대해 분석해보았다.

2. 동시제어전략

같은 사용자가 다른 환경으로 로그인을 하는 경우, http통신을 기준으로 세션을 어떻게 관리할 것인지, 말 그대로 동시적인 세션 생성에 대한 제어전략을 의미한다.

이 전략은 사용자가 동시에 세션을 생성하는 것에 대해 최대개수만큼 허용하는 것이며, http통신의 stateless 적인 한계를 보완하고 같은 사용자의 동시 요청을 허용하는 전략이다.

계정은 1개(동일한 사용자)일때 다른 환경에서 로그인을 요청하는 상황인데, Spring Security에서 제공하는 maximumSessions를 구성하여 최대허용개수를 지정해줄 수 있다.

동시적으로 세션을 생성하며 생성 후 최대허용개수를 넘었다면, 혹은 생성 시 최대허용개수를 넘게된다면,

  • 이전에 생성된 세션을 강제로 만료한다던가
  • 사용자의 인증시도를 아예 차단한다던가

의 전략을 취할 수 있다.

2-1. sessionManagement

늘 해왔듯이 httpSecurity에서 제공하는 dsl api를 통해 이를 구성해줄 수 있으며, 이 중 sessionManagement을 통해 관련 전략을 구성해줄 수 있다.

참고로 maxmimumSessions의 기본값은 무제한이기 때문에 반드시 설정해주어야 한다.

http.sessionManagement(session -> session
	.invalidSessionUrl("/invalid")
    .maxmimumSessions(1)
    .maxSessionsPreventsLogin(true)
    .expiredUrl("/expired")
    );
    
return http.build();

일단 기본적으로 maxSessionsPreventsLogin에 따라 취할 전략을 선택할 수 있다.

  • true

true를 선택할 경우, 말 그대로 로그인을 아예 차단한다.

이 경우, invalidSessionUrl, expiredUrl에서 지정한 리다이렉트 경로는 사실상 의미가 없어진다.

  • false

false를 선택할 경우, 이전의 사용자 세션정보를 만료시킨다.

이때가 중요한데, invalidSessionUrl()과 expiredUrl()을 어떻게 지정해주느냐에 따라 동작이 달라진다.

먼저 두 url을 지정해주지 않으면 session이 만료되었다는 예외 처리를 진행하며, 두 url을 모두 지정해주면 invalidSessionUrl을 우선적으로 리다이렉트한다.

invalidSessionUrl만 단독 지정할 경우 세션만료 예외처리, expiredUrl만 단독 지정할 경우 해당 url로 리다이렉트한다.

현재 확인시 .expiredUrl("/login?expired=true")만 가능하며, invalidSessionUrl은 deprecated되었다.

2-2. 세션 만료에 대한 구체적인 과정

세션을 만료한다는 것은 말 그대로 WAS에 있는 해당 세션을 "expired"처리 하는 것이고, 이는 쿠키 "삭제"등 과는 관련이 없다.

다만, Spring Security는 이러한 세션을(정확하게 말하면 이전 로그인한 인증사용자 중 가장 인증이 오래된 자를 대상으로) 만료처리를 하되, 만료세션의 대상자가 추가 요청을 해올때 ExpiredSessionStrategy가 동작하여 이 시점에서 만료동작이 진행된다.

즉, 세션만료의 "상태처리"에 대한 "실질(물리)적인 동작"은 추가적인 요청이 발생하였을때 진행한다.

이를 구체적으로 살펴보면 다음과 같다.

(1) SessionRegistry에서 해당 세션을 expired "상태"로 표시한다.

Spring Security 내부의 세션 registry(SessionRegistryImpl)에
해당 세션을 “만료됨(expired)” 상태로 설정한다.

(2) 해당 세션(을 보유한 대상)이 다음 요청을 보낼 때, Spring Security가 “ExpiredSessionStrategy”에 맞게 "물리적인 만료처리"한다.

(즉, 만료된 세션이 추가 요청을 보내기 전까지는 물리적으로 invalidate 하지 않고, 상태변화만 시켜준다)

(3) 그 후 WAS의 HttpSession.invalidate() 호출 (HTTP 요청 시점)함으로써, 만료된 상태의 세션에 대한 실질적인 처리동작을 수행한다.

Spring Security가 sessionStatus를 expired로 감지하며, SessionInformationExpiredStrategy가 동작하게 되어 최종적으로 HttpSession.invalidate() 호출, 세션이 물리적인 만료처리 및 삭제된다.

2-3. JSESSIONID 쿠키를 삭제하지는 않는다.

JessionID에 대한 쿠키는 브라우저에 계속 저장이 되며 삭제되지는 않는다.

세션이 서버에서 invalidate 되더라도 브라우저는 같은 JSESSIONID 쿠키를 유지하며, 이를 계속 서버로 보낸다.

서버는 해당 ID의 세션이 없기에 새로운 세션을 생성하고 새로운 JSESSIONID를 줄 수도 있고, 기존 cookie 값 그대로 새 세션을 만들 수도 있다.

즉, "세션 만료"는 오직 서버 측 세션 객체가 무효화되는 것이며
브라우저 쿠키를 삭제하거나 변경하는 작업을 포함하지 않는다는 점을 기억하자.

3. Session 고정공격에 대응하기 위한 전략

세션고정공격이란, 공격자가 악의적으로 미리 심어둔 세션쿠키를 사용자가 그대로 사용하여, 이후 사용자가 동작을 취하여 공격자가 해당 인증정보를 갈취하거나 악용하는 공격 방법을 말한다.

쉽게 말하면, 공격자의 세션 쿠키 ID를 그대로 사용하여 사용자가 로그인을 할 경우 공격자는 아무런 행동없이 해당 사용자의 로그인 상태를 그대로 취할 수 있게 되어 사용자 인증 정보를 탈취가능하다.

[1] Attacker -----------------> Server
          (미리 세션ID 생성 요청)

[2] Server -------------------> Attacker
          (세션ID = ABC123 발급)

[3] Attacker -----------------> User
          (특정 URL, 쿠키 등에 세션ID=ABC123을 심어 전달)

[4] User ---------------------> Server
          (로그인 요청을 함 — 하지만 세션ID=ABC123 그대로 사용)

[5] Server
    "세션ID=ABC123이 이미 있으니 그대로 로그인 정보 바인딩"

[6] User ---------------------> Server
          (정상적으로 로그인된 상태로 서비스 이용)

[7] Attacker -----------------> Server
          (같은 세션ID=ABC123로 접근 시도)

[8] Server
     "해당 세션ID는 이미 인증됨" → 공격자에게 User의 계정 접근 허용

사용자가 공격자의 세션ID를 그대로 사용하여 생기는 문제로, Spring Security는 이러한 세션 고정 공격에 대응하기 위해 로그인 시도때마다 반드시 새로운 세션ID를 생성하거나 변경하는 전략을 취한다.

이 역시도 위와 같이 sessionManagement API를 활용하여 세션고정공격에 대응할 수 있다.

http.sessionManagement((session) -> session
	.sessionFixation(sessionFixation -> sessionFixation.changeSessionId())
);

http.build();

이 전략을 사용하면 말 그대로 로그인을 할때마다 세션ID를 변경, 다만 기존 세션 자체는 그대로 유지한다.

이것이 Spring Security의 기본값이며, 웬만하면 이 방식을 그대로 실무에서는 활용한다.

http.sessionManagement((session) -> session
	.sessionFixation(sessionFixation -> sessionFixation.newSession())
);

http.build();

이 전략을 사용하면 로그인을 할 때마다 아예 새로운 세션을 생성하며, 다만 기존 세션 데이터를 복사하지 않고 SPRING_SECURITY로 시작하는 속성만 복사한다.

http.sessionManagement((session) -> session
	.sessionFixation(sessionFixation -> sessionFixation.migrateSession())
);

http.build();

새로운 세션을 생성하고, 모든 기존 세션 속성을 복사한다.

http.sessionManagement((session) -> session
	.sessionFixation(sessionFixation -> sessionFixation.none())
);

http.build();

기존 세션을 그대로 사용하며, 보안상 사용하지 말아야 한다.

4. 세션생성정책

Session Creation Policy, Spring Security 측에서는 인증사용자에 대해 어떻게 세션을 관리할지, 세션 생성에 대한 정책을 결정/관리할 수 있다.

자체적인 enum 구조를 활용하여 해당 정책을 저장하고 있다.

  • SessionCreationPolicy.ALWAYS

익명인증 사용자를 포함한 모든 행위에 대해 항상 세션을 생성하며, 이 경우 ForceEagerSessionCreationFilter를 강제적으로 추가하여 세션 생성을 항상 진행하게 된다.

  • SessionCreationPolicy.NEVER

Spring Security 측에서는 세션을 생성하지는 않는다. 다만, WAS(TOMCAT)과 같은 애플리케이션에서 생성한 세션은 사용할 수 있다.

  • SessionCreationPolicy.IF_REQUIRED

Spring Security의 기본정책이며, 로그인 등 이후 인증이 지속되어야 할 경우에만(즉, 필요한 경우에만) 세션을 생성한다.

  • SessionCreationPolicy.STATELESS

세션을 기본적으로 생성하거나 사용하지 않는 정책으로, Spring Security 측에서는 인증 완료 후 SecurityContext를 세션에 저장하지 않는다. 따라서, 세션이 유지가 되지 않으므로 매 동작마다 로그인 요청 등의 인증수행을 필요로 한다.

이에 따라 SecurityContextHolderFilter 측에서는 요청 단위로 항상 새로운 SecurityContext를 생성하며, 재활용할 세션이 저장되어있지 않기에 컨텍스트 영속성을 유지할 수 없다.

stateless 기반의 인증방식인 JWT 등에 많이 사용된다.

다만, 이러한 인증관점에서의 세션은 생성하지 않으며 CSRF 기능을 수행하기 위해 별도의 세션을 생성하여 csrf토큰에 저장하는 과정은 정상 수행한다(SecurityContext의 영속성에 영향을 미치지는 않는다).

전략 생성은 간단하다.

http.sessionManagement((session) -> session
			.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;

http.build();

위와 같이 sessionCreationPolicy에서 주어진 enum을 선택하면 된다.

5. 전략통합 - SessionManagementFilter / ConcurrentSessionFilter

지금까지 구성한 전략을 최종적으로 통합하여 Spring Security에 적용하기 위한 과정에 대해 분석해보았다.

5-1. SessionManagementFilter

요청 시 사용자의 인증여부, 인증 확인 후 세션고정공격에 대한 대책 및 동시 사용자 로그인 방지 정책 등 세션관련 정책을 통합한 SessionAuthenticationStrategy롤 호출하는 필터이다.

즉, 쉽게 말하면 sessionManagementFilter는 정책총괄자이다.

지금까지 수행한 전략구성에 대해 최종적인 전략 통합을 Spring Security에 등록하는 역할을 해주는 필터로, SessionManagementFilter는 sessionAuthenticationStrategy를 호출함으로써 동시세션제어/세션고정공격대책 등의 전략을 Security에 등록해준다.

참고로 이러한 전략은 SessionAuthenticationStrategy가 구현체 형태로 전달을 한다.

이 구현체들은

  • ChangeSessionIdAuthenticationStrategy
  • ConcurrentSessionControlAuthenticationStrategy
  • RegisterSessionAuthenticationStrategt
  • SessionFixationProtectionStrategy

의 형태로 존재하며, 반영된다.

더불어 SessionCreationPolicy 역시 이 시점에 반영하게 된다.

Spring Security 6.x 이상의 버전에서는 SessionManagementFilter가 기본설정이 아니기에, 별도의 세션관리 API를 활용하여 설정해주어야 한다.

5-2. ConcurrentSessionFilter

동시세션제어정책에 관련된 필터로, 세션만료의 "상태변경"이 아닌 실질적인 세션제거의 동작을 수행하는 필터이다.

일전에 session 만료 시 세션의 상태만 변경하고, 실질적인 동작은 세션이 만료된 사용자의 추가 행위 시 발생한다고 하였다.

이 시점에 concurrentSessionFilter가 동작함으로써, 세션 만료상태를 확인하여 만료 시 사용자는 로그아웃 및 세션 제거의 동작을 수행하는 역할을 한다.

더불어, SessionRegistry.refreshLastRequest를 통해 등록된 세션들의 최신날짜를 최신화하여 상태를 올바르게 유지할 수 있도록 하기도 한다.

5-3. 정리

두 필터의 동작에 대해 정리하자면,

  • 사용자 인증처리 후 세션만료의 상태변경은 SessionManagementFilter가 일단은 변경해준다.
  • 이후 만료된 세션을 가지는 사용자가 동작을 취하면 ConcurrentSessionFilter가 세션만료에 대한 로그아웃 및 세션제거 동작을 수행한다(이 시점에서 만료세션이 아니라면 세션최신화).

이때 세션만료 및 제거 수행 시, SessionInformationExpiredStrategy 정책을 적용한다.

또한, 세션만료정책을 적용하였다면, sessionManagementFIlter가 session.expireNow()를 호출하여 이전 세션의 상태를 만료로 변경하여 주며, 해당 세션으로 사용자 로그인 시도 시 session.isExpired()를 통해 확인절차를 거쳐서 이후의 만료 및 제거 동작을 진행한다.

[사용자 로그인]
   |
   v
(SessionManagementFilter)
 - 동시로그인 정책 적용
 - 기존 세션 만료 필요 시 session.expireNow()

[모든 요청 처리]
   |
   v
(ConcurrentSessionFilter)
 - 현재 세션의 SessionInformation 상태 확인
    ├─ EXPIRED → SessionInformationExpiredStrategy + 세션 무효화/로그아웃
    └─ ACTIVE  → lastRequestTime 업데이트

이와 같이 세션관리에 대해 최종적으로 정리할 수 있다.

참고로, session에 저장된 유저정보를 추출하고자 한다면

@Bean
    public SessionRegistry sessionRegistry(){
        return new SessionRegistryImpl();
    }

와 같이 sessionRegistry를 빈으로 등록하고, 해당 빈으로부터 session List를 받아와 session getInformation으로 인증정보를 추출해오면 된다.

6. 결론

Session은 인증을 유지할 수 있는 가장 기본적인 전략이자, 인증처리의 가장 기초적인 도구라 할 수 있겠다.

formLogin, httpBasic 등 인증처리에 대해 이해하기 위해서는 Session 및 JSession을 통한 인증세션유지 및 토큰저장까지 파악하고 있어야 한다.

지금 단계까지 Spring Security를 이해하였다면, 비로소 "인증처리에 대한 기본적 소양"을 하였다고 볼 수 있겠다.

이러한 기초소양에서 나아가, session을 어떻게 변경하고 생성해야 할지, 이에 대한 구체적이고 세부적인 전략을 구상하고 설계하는 것이 필요하겠다.

0개의 댓글