Authentication Persistence and Session Management

김상욱·2024년 12월 7일

요청이 인증되도록 설정한 애플리케이션을 확보했다면, 그 결과로 생성된 인증 정보를 어떻게 지속적으로 유지하고 이후 요청에서 복원할지를 고려하는 것이 중요합니다.

이 작업은 기본적으로 자동으로 처리되기 때문에 추가적인 코드가 필요하지 않지만, HttpSecurity에서 requireExplicitSave가 무엇을 의미하는지 알고 있는 것이 중요합니다.

원하면 requireExplicitSave가 무엇을 수행하는지 또는 왜 중요한지에 대해 더 읽을 수 있습니다. 그렇지 않으면 대부분의 경우 이 섹션에서 할 일은 끝났습니다.

그러나 이 기능 중 일부가 애플리케이션에 필요한지 고려해 보세요:

  • 세션 관리의 구성 요소를 이해하고 싶습니다.
  • 사용자가 동시에 로그인할 수 있는 횟수를 제한하고 싶습니다.
  • Spring Security 대신 직접 인증 정보를 저장하고 싶습니다.
  • 인증 정보를 수동으로 저장하고 있으며 이를 제거하고 싶습니다.
  • SessionManagementFilter를 사용하고 있으며 이를 사용하지 않는 방법에 대한 지침이 필요합니다.
  • 세션이 아닌 다른 곳에 인증 정보를 저장하고 싶습니다.
  • 무상태 인증을 사용하고 있지만 세션에 인증 정보를 저장하고 싶습니다.
  • SessionCreationPolicy.NEVER를 사용 중이지만 애플리케이션이 여전히 세션을 생성하고 있습니다.

세션 관리 구성 요소 이해하기

세션 관리 기능은 몇 가지 구성 요소로 구성되어 함께 동작하여 기능을 제공합니다. 이러한 구성 요소에는 SecurityContextHolderFilter, SecurityContextPersistenceFilter, 그리고 SessionManagementFilter가 있습니다.

Spring Security 6에서는 기본적으로 SecurityContextPersistenceFilter와 SessionManagementFilter가 설정되지 않습니다. 또한, 애플리케이션은 SecurityContextHolderFilter 또는 SecurityContextPersistenceFilter 중 하나만 설정해야 하며, 둘을 동시에 설정해서는 안 됩니다.

Spring Security의 SessionManagementFilter는 사용자의 인증 상태를 확인하고 관리하는 중요한 역할을 합니다. 이 필터는 주로 세션 기반 인증과 관련된 다양한 시나리오에서 인증 상태를 관리하고 적절한 처리를 수행합니다. 아래에 SessionManagementFilter의 동작 원리를 더 자세히 설명하겠습니다.

1. SecurityContextRepository와 SecurityContextHolder 비교

  • SessionManagementFilterSecurityContextRepository에 저장된 보안 컨텍스트와 현재 요청의 SecurityContextHolder를 비교하여 사용자가 인증되었는지를 확인합니다.
  • SecurityContextRepository는 세션이나 데이터베이스와 같은 외부 저장소에 사용자의 보안 정보를 저장하는 역할을 합니다.
  • SecurityContextHolder는 현재 요청에 대한 보안 정보를 스레드 로컬에 저장하여, 동일한 요청 내에서 사용자 인증 상태를 유지할 수 있도록 합니다.

2. 비대화식 인증 메커니즘 확인

  • SessionManagementFilter는 사용자가 인증되었는지를 판단할 때, 비대화식 인증 방식(non-interactive authentication mechanism)을 고려합니다. 비대화식 인증 방식에는 선행 인증(pre-authentication)이나 remember-me 기능이 포함됩니다.
  • 예를 들어, 사용자가 이전에 remember-me 기능을 활성화하고 로그인한 상태라면, 새 요청에서도 추가적인 로그인 절차 없이 인증 상태가 유지될 수 있습니다. SessionManagementFilter는 이런 경우를 감지하여 인증 상태를 유지시킵니다.

3. 보안 컨텍스트가 있는 경우와 없는 경우의 처리

  • 보안 컨텍스트가 있는 경우:
    • 만약 SecurityContextRepository에 보안 컨텍스트가 존재하면, 필터는 더 이상 작업을 수행하지 않고 그대로 요청을 진행시킵니다.
    • 이는 사용자가 이미 인증된 상태이며 추가적인 인증 작업이 필요하지 않음을 의미합니다.
  • 보안 컨텍스트가 없는 경우:
    • 만약 SecurityContextRepository에 보안 컨텍스트가 없지만, SecurityContextHolder에 인증 객체(Authentication)가 포함되어 있으면, 이는 이전에 인증된 것으로 간주합니다. 이 경우, SessionManagementFilter는 사용자가 이전 필터에서 인증된 것으로 판단하고, 설정된 SessionAuthenticationStrategy를 호출하여 인증된 상태로 처리합니다.

4. SessionAuthenticationStrategy 호출

  • SessionAuthenticationStrategy는 세션 기반 인증 전략을 정의합니다. 사용자가 성공적으로 인증되었을 때, 이 전략을 통해 추가적인 세션 관리 작업을 수행할 수 있습니다.
  • 예를 들어, 사용자가 동시에 여러 세션을 가질 수 없도록 제한하려는 경우, SessionAuthenticationStrategy에서 이러한 제어를 할 수 있습니다.
  • 이 전략은 세션 관리와 관련된 다양한 설정을 통해 사용자가 로그인할 때 세션을 어떻게 처리할지를 결정합니다.

5. 인증되지 않은 상태에서의 처리

  • 사용자가 인증되지 않은 상태인 경우, SessionManagementFilter는 요청된 세션 ID가 유효한지 여부를 확인합니다. 만약 잘못된 세션 ID가 요청되었거나 세션이 타임아웃된 경우, 설정된 InvalidSessionStrategy를 호출합니다.
  • InvalidSessionStrategy는 잘못된 세션이 발생했을 때의 처리 방법을 정의하는 전략입니다.
  • 가장 일반적인 동작은 사용자를 고정된 URL로 리다이렉트하는 것이며, 이를 표준 구현인 SimpleRedirectInvalidSessionStrategy가 담당합니다. 예를 들어, 세션이 만료된 경우 로그인 페이지로 리다이렉트하여 사용자가 다시 로그인할 수 있도록 할 수 있습니다.

6. SimpleRedirectInvalidSessionStrategy

  • SimpleRedirectInvalidSessionStrategyInvalidSessionStrategy의 한 구현체로, 잘못된 세션이 감지되었을 때 리다이렉트 URL을 설정하는 간단한 방식입니다.
  • 이 전략은 Spring Security의 네임스페이스를 통해 설정할 수 있으며, 특정 상황에서 유효하지 않은 세션이 발생했을 때 사용자를 지정된 URL로 안내하여 인증을 다시 진행할 수 있도록 합니다.
  • 예를 들어, 세션이 유효하지 않거나 만료된 경우 사용자에게 다시 로그인 페이지로 이동하라는 안내를 할 수 있습니다.

요약

SessionManagementFilter는 다음과 같은 순서로 동작합니다:
1. SecurityContextRepositorySecurityContextHolder를 비교하여 사용자가 인증된 상태인지 확인합니다.
2. 보안 컨텍스트가 있는 경우: 아무 작업 없이 그대로 요청을 진행시킵니다.
3. 보안 컨텍스트가 없는 경우:

  • SecurityContextHolder에 인증 객체가 있으면, 인증된 상태로 간주하고 SessionAuthenticationStrategy를 호출하여 세션 관리 작업을 수행합니다.
  • 인증되지 않은 상태이면, 잘못된 세션 ID가 요청되었는지 확인하고 InvalidSessionStrategy를 호출합니다.
  1. InvalidSessionStrategy는 유효하지 않은 세션이 발생했을 때 사용자에게 리다이렉트 URL을 제공하거나 다른 방식으로 처리합니다.

이와 같은 과정을 통해 SessionManagementFilter는 세션의 유효성을 확인하고, 사용자 인증 상태를 적절하게 관리하여 보안 수준을 유지합니다.


Spring Security 5와 Spring Security 6에서의 SessionManagementFilter 사용 방식과 그 차이점을 자세히 설명해 드리겠습니다.

1. Spring Security 5에서의 SessionManagementFilter

  • Spring Security 5에서는 SessionManagementFilter가 기본적으로 설정되어 있어, 사용자가 인증되었는지 여부를 확인하고 인증이 성공한 후의 추가 처리를 담당합니다.
  • 이 필터는 SessionAuthenticationStrategy를 호출하여, 사용자가 인증된 후 세션 관리 작업(예: 세션 고정 방지, 동시 세션 수 제한 등)을 수행하게 합니다.
  • SessionManagementFilter가 동작하기 위해서는 매 요청마다 HttpSession을 확인해야 합니다. 즉, 요청이 들어올 때마다 HttpSession이 존재하는지 읽고, 보안 컨텍스트에서 인증 상태를 확인해야 합니다.
  • 이 방식은 인증 상태를 세션을 통해 관리하는 경우에는 유용하지만, 매 요청마다 HttpSession을 읽어야 하므로 성능에 영향을 줄 수 있습니다. 특히, REST API처럼 무상태(stateless) 요청을 사용하는 애플리케이션에서는 불필요한 세션 조회로 인해 성능 저하가 발생할 수 있습니다.

2. Spring Security 6에서의 변경 사항

  • Spring Security 6에서는 SessionManagementFilter가 기본 설정에서 제외되었습니다. 대신, 인증 메커니즘 자체가 SessionAuthenticationStrategy를 호출하는 방식으로 변경되었습니다.
  • 즉, 인증 과정 중에 SessionAuthenticationStrategy가 호출되므로, 매 요청마다 HttpSession을 읽을 필요가 없어졌습니다.
  • 이제는 인증이 완료될 때만 세션을 사용하여 필요한 작업을 수행하고, 이후의 요청에서는 추가적인 세션 조회를 피할 수 있습니다.

3. 동작 방식 변화

  • Spring Security 5: SessionManagementFilter가 모든 요청에서 사용자의 인증 상태를 확인하고, 인증이 완료된 후 SessionAuthenticationStrategy를 호출하여 세션 관리 작업을 수행합니다.
  • Spring Security 6: 인증 메커니즘이 SessionAuthenticationStrategy를 직접 호출합니다. 따라서 SessionManagementFilter가 필요 없어지고, 모든 요청에서 HttpSession을 읽는 작업이 줄어듭니다.

SessionManagementFilter를 사용하지 않을 때 고려해야 할 사항

Spring Security 6에서는 SessionManagementFilter가 기본적으로 사용되지 않기 때문에, sessionManagement DSL의 일부 메서드가 효과를 발휘하지 않게 됩니다.

메서드대체 방법
sessionAuthenticationErrorUrl인증 메커니즘에서 AuthenticationFailureHandler를 구성하세요
sessionAuthenticationFailureHandler인증 메커니즘에서 AuthenticationFailureHandler를 구성하세요
sessionAuthenticationStrategy인증 메커니즘에서 SessionAuthenticationStrategy를 구성하세요 (위에서 설명한 대로)

이 메서드들을 사용하려고 하면 예외가 발생합니다.


Spring Security 6에서 도입된 Require Explicit Save 설정은 보안 컨텍스트의 저장 방식과 관련하여 중요한 개념입니다. Spring Security 5와 6에서의 차이를 살펴보며 이 설정이 왜 도입되었는지와 어떤 영향을 미치는지 자세히 설명해 드리겠습니다.

Spring Security 5의 동작 방식: 자동 저장

Spring Security 5에서는 SecurityContextPersistenceFilter가 기본적으로 사용됩니다. 이 필터는 요청이 처리되는 동안 보안 컨텍스트를 관리하며, SecurityContext를 자동으로 SecurityContextRepository에 저장합니다.

  • 자동 저장 동작:

    • SecurityContext는 HttpServletResponse가 커밋되기 직전, 즉 요청이 마무리될 때 자동으로 저장됩니다.
    • SecurityContextPersistenceFilter는 매 요청마다 SecurityContext의 상태를 추적하여, 변경이 발생할 때마다 SecurityContextRepository에 저장합니다. 일반적으로 SecurityContextRepository는 HttpSession을 통해 보안 컨텍스트를 저장합니다.
    • 이러한 자동 저장 동작은 사용자가 명시적으로 저장을 요청하지 않아도 되므로 편리할 수 있지만, 불필요한 저장 작업이 발생할 수 있으며 보안 컨텍스트가 요청 완료 전에 저장되어 의도하지 않은 결과가 발생할 수 있습니다.
  • 문제점:

    • 예상치 못한 저장 시점: SecurityContext가 자동으로 저장되기 때문에, 사용자는 보안 컨텍스트가 요청 완료 시점에 저장될 것이라고 예상할 수 있지만, 실제로는 HttpServletResponse가 커밋되기 직전에 저장됩니다. 이는 때때로 예기치 못한 결과를 초래할 수 있습니다.
    • 복잡성 및 성능 문제: 자동 저장 방식에서는 SecurityContext의 상태를 지속적으로 추적해야 하므로, 성능에 영향을 줄 수 있습니다. 상태 변경을 감지하기 위한 오버헤드가 발생하며, 필요하지 않은 경우에도 SecurityContextRepository(HttpSession 등)에 불필요한 쓰기 작업이 발생할 수 있습니다.

Spring Security 6의 동작 방식: 명시적 저장(Require Explicit Save)

Spring Security 6에서는 이러한 문제를 해결하기 위해 SecurityContextPersistenceFilter가 SecurityContextHolderFilter로 대체되었으며, Require Explicit Save 방식이 도입되었습니다.

  • 명시적 저장 방식:

    • 이제 SecurityContextHolderFilter는 요청을 처리할 때 SecurityContextRepository에서 SecurityContext를 읽어와 SecurityContextHolder에만 채워 넣는 역할만 합니다.
    • 사용자가 명시적으로 SecurityContext를 저장해야 하며, 자동으로 저장되지 않습니다. 따라서, 인증 정보를 요청 간에 유지하려면 직접 SecurityContextRepository에 저장 작업을 수행해야 합니다.
  • 명시적 저장의 장점:

    • 명확성 증가: 보안 컨텍스트가 언제 저장되는지에 대한 애매함이 없어집니다. 이제 개발자는 SecurityContext를 명시적으로 저장할 때만 SecurityContextRepository에 쓰기 작업이 이루어지기 때문에, 저장 시점을 명확하게 제어할 수 있습니다.
    • 성능 향상: 필요할 때에만 SecurityContextRepository에 쓰기를 수행하므로, 불필요한 쓰기 작업을 줄일 수 있어 성능이 향상됩니다. 이전에는 매 요청마다 상태를 추적하고 저장하는 과정에서 오버헤드가 발생했지만, 이제는 명시적인 저장 시에만 쓰기 작업이 이루어집니다.

예제 코드: SecurityContext 명시적 저장 방법

다음 예제는 Spring Security 6에서 보안 컨텍스트를 명시적으로 저장하는 방법을 보여줍니다.

private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();

@PostMapping("/login")
public void login(@RequestBody LoginRequest loginRequest, HttpServletRequest request, HttpServletResponse response) {
    UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(
        loginRequest.getUsername(), loginRequest.getPassword());
    Authentication authentication = authenticationManager.authenticate(token);
    
    // 새 보안 컨텍스트를 생성하고 인증 정보 설정
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    context.setAuthentication(authentication);
    SecurityContextHolder.setContext(context);
    
    // 보안 컨텍스트를 명시적으로 SecurityContextRepository에 저장
    securityContextRepository.saveContext(context, request, response);
}

코드 설명

  1. 보안 컨텍스트 생성 및 인증 정보 설정:

    • SecurityContextHolder.createEmptyContext()를 통해 새로운 보안 컨텍스트를 생성합니다.
    • 인증 정보를 생성한 후, 해당 인증 정보를 보안 컨텍스트에 설정합니다.
  2. SecurityContextRepository에 명시적 저장:

    • securityContextRepository.saveContext(context, request, response); 코드를 통해 보안 컨텍스트를 SecurityContextRepository(HttpSession 등)에 명시적으로 저장합니다.

요약

  • Spring Security 5: 자동 저장 방식으로, SecurityContextPersistenceFilter가 요청마다 보안 컨텍스트를 자동으로 SecurityContextRepository에 저장합니다.
  • Spring Security 6: 명시적 저장 방식으로 변경되어 SecurityContextHolderFilter가 SecurityContext를 읽기만 하고, 명시적으로 saveContext 메서드를 호출해야 저장됩니다.
  • 장점: 저장 시점의 명확성 증가, 불필요한 쓰기 작업 감소, 성능 향상.

Require Explicit Save 설정은 성능을 최적화하고, 보안 컨텍스트의 저장 시점을 명확하게 제어할 수 있도록 하여 Spring Security 애플리케이션을 더 효율적으로 관리할 수 있게 합니다.

작동 방식

요약하자면, requireExplicitSave가 true일 때 Spring Security는 SecurityContextPersistenceFilter 대신 SecurityContextHolderFilter를 설정합니다.


타임아웃 감지

세션은 자체적으로 만료되며, 보안 컨텍스트를 제거하기 위해 추가 작업을 수행할 필요는 없습니다. 그러나 Spring Security는 세션이 만료되었을 때 이를 감지하고 특정 동작을 수행하도록 설정할 수 있습니다. 예를 들어, 사용자가 만료된 세션으로 요청을 보낼 경우 특정 엔드포인트로 리다이렉트하도록 설정할 수 있습니다. 이는 HttpSecurityinvalidSessionUrl을 통해 설정됩니다.

Java 예제

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement(session -> session
            .invalidSessionUrl("/invalidSession")
        );
    return http.build();
}

주의사항

  • 세션 타임아웃 감지 시 오탐 가능성:

    • 사용자가 로그아웃한 후 브라우저를 닫지 않고 다시 로그인하면, 이전 세션의 쿠키가 삭제되지 않은 상태에서 요청이 이루어질 수 있습니다.
    • 이 경우, 세션이 이미 무효화되었기 때문에 세션 타임아웃으로 잘못 감지될 수 있습니다.
  • 해결 방법:

    • 로그아웃 시 세션 쿠키를 삭제하도록 설정합니다. 이렇게 하면 이전 세션의 쿠키가 브라우저에서 제거되어 이러한 문제가 방지됩니다.

invalidSessionUrl을 사용하면 세션이 만료되었을 때 사용자에게 명확한 안내를 제공할 수 있어, 사용자 경험을 개선할 수 있습니다.


로그아웃 시 세션 쿠키 삭제하기

로그아웃 시 JSESSIONID 쿠키를 명시적으로 삭제할 수 있습니다. 예를 들어, 로그아웃 핸들러에서 Clear-Site-Data 헤더를 사용하여 쿠키를 삭제할 수 있습니다:

Java 예제

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .logout((logout) -> logout
            .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(COOKIES)))
        );
    return http.build();
}

주요 특징

  • 컨테이너 독립적:
    • 이 방식은 특정 웹 컨테이너에 의존하지 않으며, Clear-Site-Data 헤더를 지원하는 모든 컨테이너에서 작동합니다.
    • 따라서, 컨테이너 호환성 문제가 없는 안전한 옵션입니다.

대안

로그아웃 핸들러에서 다른 방식으로도 세션 쿠키를 삭제할 수 있습니다. 이후 코드 예시를 참고하여 필요한 방식으로 구현할 수 있습니다.

Spring Security에서 로그아웃 시 세션 쿠키(JSESSIONID)를 삭제하는 방법에 대해 자세히 설명드리겠습니다.

1. Java에서 쿠키 삭제 설정

Spring Security는 deleteCookies() 메서드를 통해 로그아웃 시 특정 쿠키를 삭제하도록 설정할 수 있습니다.

코드 예제

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .logout(logout -> logout
            .deleteCookies("JSESSIONID") // JSESSIONID 쿠키 삭제 설정
        );
    return http.build();
}

동작 방식

  • deleteCookies("JSESSIONID"): 로그아웃 요청이 처리된 후, JSESSIONID 쿠키를 삭제합니다.
  • 브라우저는 삭제 명령을 받은 쿠키를 제거하여, 이후 요청에서 더 이상 세션 정보를 전달하지 않습니다.

2. 제한사항

  • 모든 서블릿 컨테이너에서 작동 보장 불가:
    • 이 방법은 표준적으로 제공되지만, 일부 서블릿 컨테이너(예: 특정 WAS 또는 환경)에서는 제대로 동작하지 않을 수 있습니다.
    • 따라서, 사용하는 환경에서 쿠키 삭제가 제대로 작동하는지 테스트가 필수입니다.

3. 프록시 서버를 이용한 쿠키 삭제

만약 애플리케이션이 프록시 뒤에서 실행되고 있다면, 프록시 서버를 구성하여 쿠키를 삭제할 수도 있습니다.

Apache HTTPD 예제

다음은 Apache HTTPD에서 mod_headers 모듈을 사용해 로그아웃 요청의 응답에서 JSESSIONID 쿠키를 만료시키는 설정 예제입니다.

<LocationMatch "/tutorial/logout">
Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"
</LocationMatch>

구성 설명

  • <LocationMatch>: 특정 경로(/tutorial/logout)에 대해 설정을 적용합니다.
  • Header always set:
    • HTTP 응답 헤더에 Set-Cookie 값을 추가합니다.
    • JSESSIONID=;: 쿠키 값을 빈 값으로 설정합니다.
    • Path=/tutorial;: 쿠키의 유효 경로를 애플리케이션의 경로(/tutorial)로 제한합니다.
    • Expires=Thu, 01 Jan 1970 00:00:00 GMT;: 만료일을 과거로 설정하여 쿠키를 즉시 만료시킵니다.

주요 장점

  • 프록시 기반 제어: 애플리케이션 레벨이 아니라 프록시 레벨에서 제어하므로, 추가적인 코드 변경 없이 쿠키 관리가 가능합니다.
  • 컨테이너 독립성: 서블릿 컨테이너의 동작에 영향을 받지 않습니다.

4. Clear-Site-Data 헤더를 사용한 쿠키 삭제

Spring Security에서는 Clear-Site-Data 헤더를 활용하여 쿠키를 포함한 데이터를 삭제할 수도 있습니다.

코드 예제

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .logout((logout) -> logout
            .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(COOKIES)))
        );
    return http.build();
}

Clear-Site-Data 헤더 설명

  • Clear-Site-Data:
    • 웹 브라우저에게 쿠키, 캐시, 스토리지 데이터를 삭제하라고 지시하는 HTTP 응답 헤더입니다.
    • COOKIES: 브라우저 쿠키를 삭제합니다.

주요 장점

  • 컨테이너 독립적:
    • 특정 서블릿 컨테이너에 의존하지 않으며, Clear-Site-Data 헤더를 지원하는 모든 브라우저에서 작동합니다.
  • 포괄적 데이터 삭제:
    • 쿠키뿐만 아니라 캐시, 로컬 스토리지 등도 삭제할 수 있어, 보다 철저한 클린업이 가능합니다.

5. 각 방법 비교 및 선택

방법장점제한사항
deleteCookies 메서드코드 수준에서 간단히 설정 가능모든 서블릿 컨테이너에서 동작을 보장할 수 없으므로 테스트 필요
프록시 서버(Apache HTTPD)프록시 레벨에서 제어하므로 애플리케이션 코드 수정 불필요프록시 설정에 대한 추가 작업 필요, 프록시가 없는 경우 사용할 수 없음
Clear-Site-Data 헤더컨테이너 독립적이며, 쿠키 외에도 캐시 및 스토리지 삭제 가능Clear-Site-Data 헤더를 지원하지 않는 브라우저나 환경에서는 작동하지 않을 수 있음

6. 결론

  • 간단한 애플리케이션:
    • deleteCookies("JSESSIONID")를 사용하여 Spring Security 수준에서 해결.
  • 프록시가 있는 경우:
    • Apache HTTPD 설정을 통해 쿠키 만료 처리.
  • 철저한 데이터 삭제 필요:
    • Clear-Site-Data 헤더를 활용하여 쿠키뿐만 아니라 캐시와 스토리지까지 삭제.

각 환경에 맞는 방법을 선택하여 구현하면, 로그아웃 시 세션 쿠키 삭제를 안전하고 효율적으로 처리할 수 있습니다.


세션 고정 공격 보호 이해하기

세션 고정 공격(Session Fixation Attack)은 악의적인 공격자가 사이트에 접근하여 세션을 생성한 뒤, 다른 사용자에게 동일한 세션을 사용하도록 유도하는 방식으로 이루어질 수 있습니다. 예를 들어, 공격자는 세션 식별자를 URL 파라미터로 포함한 링크를 피해자에게 보내 로그인하도록 유도할 수 있습니다.

Spring Security는 사용자가 로그인할 때 새로운 세션을 생성하거나 세션 ID를 변경하여 이러한 공격을 자동으로 방지합니다.

세션 고정 보호 전략 설정

Spring Security에서 세션 고정 보호를 위한 전략은 다음 세 가지로 설정할 수 있습니다:

1. changeSessionId (기본값)

  • 새로운 세션을 생성하지 않고, 현재 세션의 ID를 변경합니다.
  • 이 방식은 서블릿 컨테이너가 제공하는 HttpServletRequest#changeSessionId() 메서드를 사용하여 구현됩니다.
  • 요구사항:
    • Servlet 3.1(Java EE 7) 이상에서만 사용할 수 있습니다. 더 오래된 컨테이너에서 설정하면 예외가 발생합니다.
    • Servlet 3.1 이상에서는 기본값으로 설정됩니다.

2. newSession

  • "깨끗한" 새 세션을 생성합니다.
  • 기존 세션 데이터를 복사하지 않지만, Spring Security와 관련된 속성은 복사됩니다.
  • 사용자 정의 데이터가 필요한 경우 추가 작업이 필요할 수 있습니다.

3. migrateSession

  • 새로운 세션을 생성하고, 기존 세션의 모든 속성을 새로운 세션으로 복사합니다.
  • Servlet 3.0 이하의 컨테이너에서는 기본값으로 설정됩니다.

세션 고정 보호 설정 예제

Java 예제

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement((session) -> session
            .sessionFixation((sessionFixation) -> sessionFixation
                .newSession() // 새로운 세션을 생성
            )
        );
    return http.build();
}

설명

  • newSession(): 로그인이 이루어질 때 기존 세션을 사용하지 않고 새 세션을 생성합니다.
  • 설정된 전략에 따라 Spring Security는 세션 고정 공격으로부터 애플리케이션을 보호합니다.

세션 고정 보호 동작

  • 이벤트 발생:
    • 세션 고정 보호가 발생하면 애플리케이션 컨텍스트에 SessionFixationProtectionEvent가 발행됩니다.
  • changeSessionId 사용 시:
    • 이 보호 방식은 jakarta.servlet.http.HttpSessionIdListener도 함께 알림을 받습니다. 따라서 두 이벤트를 동시에 수신하도록 설정된 경우, 적절히 관리해야 합니다.

세션 고정 보호 비활성화

.sessionFixation((sessionFixation) -> sessionFixation.none())
  • 비활성화: 세션 고정 보호를 완전히 끄는 설정입니다.
  • 비추천: 이 설정은 애플리케이션이 세션 고정 공격에 취약해질 수 있으므로 사용하지 않는 것이 좋습니다.

요약

  • Spring Security는 세션 고정 공격을 방지하기 위해 세션 ID를 변경하거나 새로운 세션을 생성합니다.
  • 기본 전략은 서블릿 컨테이너 버전에 따라 changeSessionId 또는 migrateSession으로 설정됩니다.
  • 세션 고정 보호를 비활성화하지 않는 것이 권장되며, 애플리케이션 요구사항에 따라 적절한 전략을 선택해야 합니다.

SecurityContextHolderStrategy 사용하기

다음 코드는 SecurityContextHolder를 사용하여 보안 컨텍스트를 설정하는 방법을 보여줍니다:

Java 코드 예제

UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
        loginRequest.getUsername(), loginRequest.getPassword());
Authentication authentication = this.authenticationManager.authenticate(token);
// ...
SecurityContext context = SecurityContextHolder.createEmptyContext(); 
context.setAuthentication(authentication); 
SecurityContextHolder.setContext(context); 

코드 동작 설명

  1. SecurityContext의 빈 인스턴스 생성:

    • SecurityContextHolder.createEmptyContext()를 호출하여 빈 SecurityContext 인스턴스를 생성합니다.
    • 이 단계는 현재 요청을 처리하는 동안 사용할 보안 컨텍스트를 초기화하는 작업입니다.
  2. Authentication 객체 설정:

    • context.setAuthentication(authentication)를 통해 인증된 Authentication 객체를 SecurityContext에 설정합니다.
    • 이 작업은 인증된 사용자 정보를 보안 컨텍스트에 추가합니다.
  3. SecurityContext를 SecurityContextHolder에 설정:

    • SecurityContextHolder.setContext(context)를 호출하여 SecurityContextSecurityContextHolder에 저장합니다.
    • SecurityContextHolder는 정적(static)으로 관리되며, 애플리케이션의 현재 스레드에 보안 컨텍스트를 저장합니다.

요약

  • SecurityContextHolder를 정적으로 호출하여 빈 SecurityContext를 생성합니다.
  • 생성된 SecurityContext에 인증 객체를 설정합니다.
  • 설정된 SecurityContextSecurityContextHolder에 저장하여 현재 요청에서 사용 가능하게 합니다.

Spring Security에서 SecurityContextHolderSecurityContextHolderStrategy를 다루는 방식에 대해 더 깊이 설명하겠습니다. 특히, 기존 코드의 문제점과 이를 해결하기 위해 SecurityContextHolderStrategy를 주입받아 사용하는 방법을 자세히 다뤄보겠습니다.

1. SecurityContextHolderSecurityContextHolderStrategy 기본 개념

1.1. SecurityContextHolder

  • Spring Security에서 인증 정보를 저장하는 전역(static) 컨텍스트 관리 클래스입니다.
  • SecurityContext 객체를 스레드 로컬(Thread Local)에 저장하여, 현재 스레드에서 사용되는 인증 정보에 접근할 수 있습니다.
  • 기본 동작은 아래와 같습니다:
    1. SecurityContextHolder를 통해 빈 SecurityContext를 생성.
    2. 생성된 SecurityContextAuthentication 객체를 설정.
    3. 설정된 SecurityContext를 다시 SecurityContextHolder에 저장.

1.2. SecurityContextHolderStrategy

  • SecurityContextHolder의 동작 방식을 결정하는 전략 객체입니다.
  • Spring Security는 클래스 로더(classloader)당 하나의 SecurityContextHolderStrategy를 설정합니다.
  • 예를 들어, 기본적으로 ThreadLocalSecurityContextHolderStrategy를 사용하여 인증 정보를 스레드 로컬에 저장합니다.
  • 다른 컨텍스트마다 독립적인 동작을 원한다면, 이 전략을 사용자 정의하거나 주입받아 사용해야 합니다.

2. 기존 코드와 문제점

기존 코드

SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);

문제점

  1. 정적 접근으로 인한 경합:

    • SecurityContextHolder는 클래스 로더당 하나의 SecurityContextHolderStrategy를 사용하므로, 여러 애플리케이션 컨텍스트가 같은 전략을 공유합니다.
    • 동일한 클래스 로더를 사용하는 여러 컨텍스트에서 서로 다른 전략을 설정하면, 경합(race condition)이 발생할 수 있습니다.
  2. 전역 상태 관리의 비효율성:

    • SecurityContextHolder에 직접 접근하면, 애플리케이션 테스트나 멀티 컨텍스트 환경에서 의도하지 않은 동작이 발생할 수 있습니다.
    • 예: 테스트 환경에서 하나의 컨텍스트가 전략을 변경하면 다른 컨텍스트에 영향을 줄 수 있음.
  3. 유연성 부족:

    • 정적 메서드를 통해 SecurityContextHolder를 사용하는 방식은 전략 변경이나 커스터마이징에 제약이 있습니다.
    • 컨텍스트마다 다른 SecurityContextHolderStrategy를 사용해야 하는 경우 설정이 어렵습니다.

3. 문제 해결: SecurityContextHolderStrategy를 주입받아 사용

Spring Security는 SecurityContextHolder를 통해 정적으로 접근하는 대신, SecurityContextHolderStrategy를 주입받아 사용하는 방식을 권장합니다.

코드 예제

public class SomeClass {

    // SecurityContextHolder에서 현재 전략을 가져와 저장
    private final SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();

    public void someMethod() {
        // 인증 토큰 생성
        UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(
                loginRequest.getUsername(), loginRequest.getPassword());
        // 사용자 인증 수행
        Authentication authentication = this.authenticationManager.authenticate(token);
        
        // 빈 SecurityContext 생성
        SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
        context.setAuthentication(authentication); // 인증 정보 설정
        this.securityContextHolderStrategy.setContext(context); // SecurityContext 저장
    }
}

4. 새 코드의 동작 방식

  1. SecurityContextHolderStrategy를 주입받아 사용:

    • SecurityContextHolder.getContextHolderStrategy() 메서드를 호출하여 현재 애플리케이션 컨텍스트에서 사용할 SecurityContextHolderStrategy를 가져옵니다.
    • 각 컨텍스트는 자신만의 SecurityContextHolderStrategy를 독립적으로 사용할 수 있습니다.
  2. SecurityContext 생성 및 설정:

    • securityContextHolderStrategy.createEmptyContext()를 호출하여 빈 SecurityContext를 생성합니다.
    • 인증된 Authentication 객체를 설정합니다.
  3. SecurityContext 저장:

    • securityContextHolderStrategy.setContext(context)를 호출하여 SecurityContext를 설정합니다.
    • 정적 접근 방식 대신 SecurityContextHolderStrategy를 통해 저장하므로, 클래스 로더 충돌 없이 안전하게 사용할 수 있습니다.

5. 새 방식의 주요 장점

  1. 컨텍스트 간 독립성:

    • 각 애플리케이션 컨텍스트가 독립적인 SecurityContextHolderStrategy를 사용하므로, 경합 조건(race condition)이 제거됩니다.
  2. 유연성 증가:

    • 주입받은 SecurityContextHolderStrategy를 커스터마이징하거나 교체할 수 있으므로, 테스트와 확장성이 좋아집니다.
  3. 전역 상태 의존성 제거:

    • 정적 접근 방식의 전역 상태 관리 문제를 방지하여, 보다 명확하고 안정적인 동작을 보장합니다.
  4. 테스트 용이성:

    • 테스트 환경에서 독립적인 SecurityContextHolderStrategy를 주입받아 사용할 수 있어, 테스트 결과의 신뢰도가 높아집니다.

6. 요약

기존 방식의 문제를 해결하기 위해:

  • SecurityContextHolder의 정적 접근을 피하고, SecurityContextHolderStrategy를 사용합니다.
  • 이 방식을 통해 컨텍스트 간 충돌을 방지하고, 더 유연하고 안정적인 코드 구조를 설계할 수 있습니다.
  • Spring Security에서 권장하는 이 방식은 특히 멀티 애플리케이션 컨텍스트 환경에서 효과적입니다.

세션을 강제로 즉시 생성하기

때로는 세션을 즉시 생성(eager session creation)하는 것이 유용할 수 있습니다. Spring Security에서는 이를 위해 ForceEagerSessionCreationFilter를 사용할 수 있으며, 다음과 같이 구성할 수 있습니다:

Java 예제

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.ALWAYS) // 세션을 항상 즉시 생성
        );
    return http.build();
}

동작 설명

  • SessionCreationPolicy.ALWAYS:
    • 요청이 들어올 때마다 세션이 즉시 생성됩니다.
    • 사용자가 인증되지 않았더라도 세션이 생성되므로, 세션이 필요하지 않은 요청에서도 생성될 수 있습니다.

사용 사례

  • 세션 초기화가 반드시 필요한 특정 비즈니스 요구사항이 있는 경우.
  • 사용자 인증 여부와 상관없이 세션에 데이터를 저장해야 하는 경우.

참고

  • Spring Session을 사용하여 클러스터링된 세션을 구성하거나, 더 복잡한 세션 관리 요구사항을 처리할 수도 있습니다.

0개의 댓글