Java & Spring/Spring Security

05. 세션 관리

승주우에요 2026. 8. 5. 11:14
세션 관리 - Spring Security

세션 관리

Spring Security의 세션 생성 정책, 고정 보호, 동시 세션 제어 전략

Spring Security의 세션 관리는 인증된 사용자의 세션을 안전하게 유지하기 위한 여러 전략을 제공한다. 동시 세션 제어를 통해 동일 계정의 중복 로그인을 관리하고, 세션 고정 보호로 세션 하이재킹 공격을 방어하며, SessionCreationPolicy를 통해 세션 생성 방식을 세밀하게 제어할 수 있다. 이 모든 처리는 SessionManagementFilterConcurrentSessionFilter를 통해 이루어진다.

1. 동시 세션 제어

개념

동시 세션 제어는 동일한 사용자 계정이 여러 브라우저나 디바이스에서 동시에 세션을 생성하는 것을 관리하는 전략이다. 인증이 완료된 시점에 활성화된 세션의 수를 maximumSessions 값과 비교하여 제어 여부를 결정한다.

2가지 제어 유형

  • 사용자 세션 강제 만료: 최대 허용 세션 수를 초과하면 기존 사용자의 세션을 만료시키고 신규 로그인을 허용한다 (maxSessionsPreventsLogin(false), 기본값)
  • 사용자 인증 시도 차단: 최대 허용 세션 수에 도달한 경우 새로운 인증 시도 자체를 차단한다 (maxSessionsPreventsLogin(true))

동작 흐름

flowchart TD A([사용자 로그인 시도]) --> B{현재 세션 수 확인} B -->|세션 수 < maxSessions| C[로그인 허용\n세션 생성] B -->|세션 수 >= maxSessions| D{maxSessionsPreventsLogin} D -->|false 기본값| E[기존 세션 강제 만료\nsession.expireNow] D -->|true| F[SessionAuthenticationException\n인증 차단] E --> G[신규 세션 생성\n로그인 성공] classDef client fill:#1c3a5f,stroke:#58a6ff,color:#ffffff classDef filter fill:#3d1f5c,stroke:#bc8cff,color:#ffffff classDef success fill:#1a3a2a,stroke:#3fb950,color:#ffffff classDef fail fill:#5c1a1a,stroke:#f85149,color:#ffffff classDef neutral fill:#2d333b,stroke:#768390,color:#e6edf3 class A client class B,D filter class C,G success class F fail class E neutral

핵심 코드

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.sessionManagement(session -> session
            .invalidSessionUrl("/invalidSessionUrl")  // 만료된 세션으로 요청 시 리다이렉트할 URL
            .maximumSessions(1)                        // 사용자당 최대 세션 수 (기본값: 무제한)
            .maxSessionsPreventsLogin(true)            // true: 최대 세션 초과 시 신규 로그인 차단
                                                       // false(기본): 신규 로그인 허용, 기존 세션 만료
            .expiredUrl("/expired")                    // 세션 만료 후 리다이렉션 URL
    );
    return http.build();
}

세션 만료 후 리다이렉션 전략

maxSessionsPreventsLogin() invalidSessionUrl() expiredUrl() 결과
falseXXThis session has been expired
OXThis session has been expired
OOinvalidSessionUrl()에 설정된 URL로 리다이렉션
XOexpiredUrl()에 설정된 URL로 리다이렉션
trueO, XO, X인증이 차단된다
invalidSessionUrl()은 세션 자체가 유효하지 않을 때(만료 포함) 적용되고, expiredUrl()은 동시 세션 제어로 인해 세션이 만료되었을 때 적용된다. 두 URL이 모두 설정된 경우 invalidSessionUrl()이 우선된다.

2. 세션 고정 보호

개념

세션 고정 공격(Session Fixation Attack)은 공격자가 먼저 사이트에 접속하여 세션을 획득한 뒤, 그 세션 ID를 피해자에게 심어서 피해자가 해당 세션으로 로그인하도록 유도하는 공격 기법이다. 공격자는 피해자가 로그인한 세션 ID를 그대로 사용하여 피해자의 모든 권한과 정보에 접근할 수 있다.

스프링 시큐리티는 사용자가 로그인할 때 새로운 세션을 생성하거나 세션 ID를 변경함으로써 이러한 공격에 자동으로 대응한다.

세션 고정 공격 흐름

sequenceDiagram participant A as 공격자 participant U as 사용자 participant S as 사이트 A->>S: 사이트 접속 S-->>A: 세션 쿠키 전송 (JSESSIONID=123456) A->>U: 공격자 세션 쿠키 심음 (JSESSIONID=123456) U->>S: 공격자 세션 쿠키로 로그인 시도 (JSESSIONID=123456) S-->>U: 로그인 성공 (공격자 세션으로 인증됨) A->>S: 사이트 접속 (JSESSIONID=123456) Note over A,S: 공격자는 사용자의 모든 권한과 정보를 공유한다

보호 전략

  • changeSessionId(): 기존 세션을 유지하면서 세션 ID만 변경한다. 기본값으로 설정되어 있다
  • newSession(): 새로운 세션을 생성하고 기존 세션 데이터를 복사하지 않는다 (SPRING_SECURITY_로 시작하는 속성은 복사)
  • migrateSession(): 새로운 세션을 생성하고 모든 기존 세션 속성을 새 세션으로 복사한다
  • none(): 기존 세션을 그대로 사용한다 (세션 고정 공격에 취약하므로 비권장)

핵심 코드

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.sessionManagement(session -> session
            .sessionFixation(sessionFixation -> sessionFixation.newSession())
            // changeSessionId() — 기본값, 세션 ID만 변경 (세션 데이터 유지)
            // newSession()      — 새 세션 생성, 기존 데이터 미복사
            // migrateSession()  — 새 세션 생성, 기존 데이터 전체 복사
            // none()            — 세션 변경 없음 (비권장)
    );
    return http.build();
}

내부 동작 코드 레벨

세션 고정 보호는 ChangeSessionIdAuthenticationStrategy 또는 SessionFixationProtectionStrategy가 담당한다. 인증 성공 직후 SessionAuthenticationStrategy.onAuthentication()이 호출되면서 세션 ID를 갱신한다.

// AbstractSessionFixationProtectionStrategy — 공통 처리 (요약)
public void onAuthentication(Authentication authentication,
                              HttpServletRequest request, HttpServletResponse response) {
    boolean hadSessionAlready = request.getSession(false) != null;
    if (!hadSessionAlready) {
        return; // 세션이 없으면 처리 불필요
    }
    // 세션이 존재하는 경우에만 세션 고정 보호 전략 적용
    applySessionFixation(request);
    onSessionChange(originalSessionId, newSession, authentication);
}

// ChangeSessionIdAuthenticationStrategy — changeSessionId() 전략
@Override
protected void applySessionFixation(HttpServletRequest request) {
    // 기존 세션 속성을 그대로 유지하면서 세션 ID만 교체
    request.changeSessionId();
}

3. 세션 생성 정책

개념

스프링 시큐리티는 SessionCreationPolicy를 통해 세션을 언제, 어떻게 생성할지 정책을 설정할 수 있다. 애플리케이션의 인증 방식(폼 로그인, JWT 등)에 따라 적합한 정책을 선택한다.

정책 종류

  • ALWAYS: 인증 여부에 상관없이 항상 세션을 생성한다. ForceEagerSessionCreationFilter 클래스를 추가 구성하여 세션을 강제로 생성시킨다.
  • NEVER: 스프링 시큐리티가 세션을 생성하지 않는다. 단, 애플리케이션이 이미 생성한 세션은 사용할 수 있다.
  • IF_REQUIRED: 필요한 경우에만 세션을 생성한다. 예를 들어 인증이 필요한 자원에 접근할 때 세션을 생성한다. 기본값이다.
  • STATELESS: 세션을 전혀 생성하거나 사용하지 않는다. JWT와 같이 세션 없이 인증을 관리할 때 적합하다. 인증 필터는 인증 완료 후 SecurityContext를 세션에 저장하지 않으며, SecurityContextHolderFilter는 요청 단위로 항상 새로운 SecurityContext를 생성하므로 컨텍스트 영속성이 유지되지 않는다.
STATELESS 설정에도 세션이 생성될 수 있다.
CSRF 기능이 활성화되어 있고 CSRF 토큰을 세션에 저장하는 경우, 세션이 생성된다. 단, 이 세션은 CSRF 토큰 저장 목적으로만 사용되며 인증 프로세스의 SecurityContext 영속성에는 영향을 미치지 않는다.

핵심 코드

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            // ALWAYS      — 항상 세션 생성
            // NEVER       — 시큐리티가 세션 생성 안함, 기존 세션은 사용
            // IF_REQUIRED — 필요한 경우에만 생성 (기본값)
            // STATELESS   — 세션 전혀 생성/사용 안함 (JWT 등 무상태 인증)
    );
    return http.build();
}

4. SessionManagementFilter & ConcurrentSessionFilter

SessionManagementFilter

요청이 시작된 이후 사용자가 인증되었는지 감지하고, 인증된 경우에는 세션 고정 보호 메커니즘 활성화나 동시 다중 로그인 확인 등 세션 관련 활동을 수행하기 위해 설정된 SessionAuthenticationStrategy를 호출하는 필터 클래스이다.

스프링 시큐리티 6 이상에서는 SessionManagementFilter가 기본으로 설정되지 않으며, 세션 관리 API를 명시적으로 설정해야 활성화된다.

세션 구성 요소

flowchart TD SMF[SessionManagementFilter] --> SAS[SessionAuthenticationStrategy] SAS --> CSID[ChangeSessionIdAuthenticationStrategy\n세션 아이디 변경] SAS --> CSCAS[ConcurrentSessionControlAuthenticationStrategy\n동시 세션 제어] SAS --> RSAS[RegisterSessionAuthenticationStrategy\n세션 정보 관리] SAS --> SFPS[SessionFixationProtectionStrategy\n세션 고정 보호] SMF --> SCP[SessionCreationPolicy] SCP --> POLICIES[ALWAYS / NEVER / IF_REQUIRED / STATELESS] classDef filter fill:#3d1f5c,stroke:#bc8cff,color:#ffffff classDef strategy fill:#1c3a5f,stroke:#58a6ff,color:#ffffff classDef policy fill:#1a3a2a,stroke:#3fb950,color:#ffffff classDef neutral fill:#2d333b,stroke:#768390,color:#e6edf3 class SMF,SAS filter class CSID,CSCAS,RSAS,SFPS strategy class SCP policy class POLICIES neutral

ConcurrentSessionFilter

각 요청에 대해 SessionRegistry에서 SessionInformation을 검색하고 세션이 만료로 표시되었는지 확인한다. 만료로 표시된 경우 로그아웃 처리를 수행하여 세션을 무효화한다.

또한 각 요청에 대해 SessionRegistry.refreshLastRequest(String)를 호출하여 등록된 세션들이 항상 '마지막 업데이트' 날짜/시간을 가지도록 한다.

ConcurrentSessionFilter 흐름도

flowchart TD A([사용자 - 인증 요청]) --> SMF[SessionManagementFilter] B([사용자 - 재 접속]) --> CSF[ConcurrentSessionFilter] SMF -->|세션 허용 개수 초과| EXP["session.expireNow()\n세션 만료 설정"] CSF --> CHECK{"session.isExpired()\n세션 만료 확인"} CHECK -->|N| UPD[세션 날짜/시간 업데이트] CHECK -->|Y| LOGOUT[Logout] LOGOUT --> STRAT["SessionInformationExpiredStrategy\n만료된 세션을 처리"] classDef client fill:#1c3a5f,stroke:#58a6ff,color:#ffffff classDef filter fill:#3d1f5c,stroke:#bc8cff,color:#ffffff classDef success fill:#1a3a2a,stroke:#3fb950,color:#ffffff classDef fail fill:#5c1a1a,stroke:#f85149,color:#ffffff classDef neutral fill:#2d333b,stroke:#768390,color:#e6edf3 class A,B client class SMF,CSF filter class EXP fail class UPD success class CHECK,LOGOUT,STRAT neutral

동시 세션 시퀀스 다이어그램

sequenceDiagram participant U1 as user1 participant U2 as user2 participant CSF as ConcurrentSessionFilter participant RS as RegisterSession\nAuthenticationStrategy participant CHS as ChangeSessionId\nAuthenticationStrategy participant CSS as ConcurrentSessionControl\nAuthenticationStrategy U1->>CSS: GET /login (session count 0) CSS-->>CHS: 세션 허용 확인 CHS->>CHS: session.changeSessionId() CHS->>RS: 세션 정보 등록: session 1 U2->>CSS: GET /login (session count 1 == maxSessions) Note over CSS: 인증 시도 차단 전략인 경우 CSS-->>U2: SessionAuthenticationException (인증 실패) Note over CSS: 세션 만료 전략인 경우 CSS->>CSS: session.expireNow() — user1 세션 만료 CHS->>CHS: session.changeSessionId() CHS->>RS: 세션 정보 등록: session 2 U1->>CSF: GET /home (재접속) CSF->>CSS: session.isExpired() 확인 CSS-->>CSF: 만료됨 CSF-->>U1: Logout & Response "This session has been expired"

내부 동작 코드 레벨

ConcurrentSessionFilter는 필터 체인에서 각 요청마다 세션 만료 여부를 확인한다.

// ConcurrentSessionFilter 내부 동작 요약
public class ConcurrentSessionFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpSession session = req.getSession(false);

        if (session != null) {
            SessionInformation info = sessionRegistry.getSessionInformation(session.getId());
            if (info != null) {
                if (info.isExpired()) {
                    // 세션이 만료된 경우: 로그아웃 처리 후 만료 응답을 반환한다
                    doLogout(req, (HttpServletResponse) response);
                    sessionInformationExpiredStrategy.onExpiredSessionDetected(
                        new SessionInformationExpiredEvent(info, req, (HttpServletResponse) response));
                    return;
                }
                // 세션이 유효한 경우: 마지막 요청 시간을 갱신한다
                sessionRegistry.refreshLastRequest(info.getSessionId());
            }
        }
        chain.doFilter(request, response);
    }
}

ConcurrentSessionControlAuthenticationStrategy는 인증 시점에 세션 수를 제어한다.

// ConcurrentSessionControlAuthenticationStrategy 내부 동작 요약
public class ConcurrentSessionControlAuthenticationStrategy implements SessionAuthenticationStrategy {

    @Override
    public void onAuthentication(Authentication authentication,
                                  HttpServletRequest request, HttpServletResponse response) {
        List<SessionInformation> sessions = sessionRegistry.getAllSessions(
                authentication.getPrincipal(), false);

        int sessionCount = sessions.size();
        int allowedSessions = getMaximumSessionsForThisUser(authentication);

        if (sessionCount < allowedSessions) {
            return; // 세션 수 범위 내: 정상 처리
        }
        if (allowedSessions == -1) {
            return; // 무제한 허용
        }
        if (sessionCount == allowedSessions) {
            // maxSessionsPreventsLogin(true): 신규 인증 차단
            if (exceptionIfMaximumExceeded) {
                throw new SessionAuthenticationException("Maximum sessions exceeded");
            }
            // maxSessionsPreventsLogin(false): 기존 세션 만료 처리
            allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry);
        }
    }
}

정리

  • 동시 세션 제어: maximumSessions로 최대 세션 수를 제한하고, maxSessionsPreventsLogin으로 기존 세션 만료 또는 신규 로그인 차단 전략을 선택한다
  • 세션 고정 보호: 기본값은 changeSessionId()로 로그인 시 세션 ID를 변경하여 세션 고정 공격을 방어한다. 공격자가 심어 놓은 세션 ID는 로그인 후 무효화된다
  • 세션 생성 정책: JWT 등 무상태 인증에는 STATELESS, 일반 폼 로그인에는 IF_REQUIRED(기본값)를 사용한다. STATELESS에서도 CSRF 토큰을 세션에 저장하는 경우 세션이 생성될 수 있다
  • SessionManagementFilter: Spring Security 6+에서는 기본으로 등록되지 않으며, 세션 관리 API를 명시적으로 설정해야 활성화된다. SessionAuthenticationStrategy를 통해 세션 아이디 변경, 동시 세션 제어, 세션 정보 등록, 세션 고정 보호를 처리한다
  • ConcurrentSessionFilter: 각 요청마다 SessionRegistry를 조회하여 세션 만료 여부를 확인하고, 만료된 세션은 즉시 로그아웃 처리하여 "This session has been expired" 응답을 반환한다

'Java & Spring > Spring Security' 카테고리의 다른 글

07. 악용 보호  (0) 2026.08.06
06. 예외 처리  (0) 2026.08.05
04. 인증 상태 영속성  (0) 2026.08.04
03. 인증 아키텍처  (0) 2026.08.04
02. 인증 프로세스  (0) 2026.08.01