Persisting Authentication

김상욱·2024년 12월 7일

사용자가 보호된 리소스를 처음 요청할 때 자격 증명이 요구됩니다. 자격 증명을 요청하는 가장 일반적인 방법 중 하나는 사용자를 로그인 페이지로 리디렉션하는 것입니다. 인증되지 않은 사용자가 보호된 리소스를 요청할 때의 요약된 HTTP 교환은 다음과 같습니다.

이 예시는 인증되지 않은 사용자가 보호된 리소스에 접근하는 과정을 설명합니다. 전체 흐름을 순서대로 자세히 설명드리겠습니다.

  1. 초기 요청 - 인증되지 않은 사용자:

    • 사용자가 보호된 리소스를 요청합니다.
    • 예시 요청:
      GET / HTTP/1.1
      Host: example.com
      Cookie: SESSION=91470ce0-3f3c-455b-b7ad-079b02290f7b
    • 이 요청은 사용자의 SESSION ID가 포함되어 있지만, 현재 상태는 인증되지 않았습니다.
  2. 서버 응답 - 로그인 페이지로 리디렉션:

    • 서버는 사용자가 인증되지 않았음을 인지하고, 로그인 페이지로 리디렉션하는 응답을 보냅니다.
    • 예시 응답:
      HTTP/1.1 302 Found
      Location: /login
    • 302 Found 응답 코드는 리디렉션을 의미하며, Location 헤더에 사용자가 로그인 페이지(/login)로 이동하도록 지시합니다.
  3. 사용자 자격 증명 제출 - 로그인 요청:

    • 사용자는 POST 요청을 통해 사용자 이름과 비밀번호를 제출하여 로그인합니다. 이 요청은 인증을 위해 서버에 전송됩니다.

    • 예시 요청:

      POST /login HTTP/1.1
      Host: example.com
      Cookie: SESSION=91470ce0-3f3c-455b-b7ad-079b02290f7b
      
      username=user&password=password&_csrf=35942e65-a172-4cd4-a1d4-d16a51147b3e
    • 요청 본문에는 username(사용자 이름)과 password(비밀번호)가 포함되어 있으며, _csrf 토큰이 포함됩니다. CSRF 토큰은 보안 강화를 위한 추가 요소입니다.

  4. 서버 응답 - 새로운 세션 ID 할당:

    • 서버는 인증에 성공하면, 보안을 강화하기 위해 새로운 세션 ID를 발급하여 사용자에게 할당합니다. 이는 세션 고정 공격(session fixation attack)을 방지하는 효과가 있습니다.
    • 예시 응답:
      HTTP/1.1 302 Found
      Location: /
      Set-Cookie: SESSION=4c66e474-3f5a-43ed-8e48-cc1d8cb1d1c8; Path=/; HttpOnly; SameSite=Lax
    • 새로운 SESSION 쿠키가 할당되며, 이제 사용자는 인증된 상태로 보호된 리소스에 접근할 수 있습니다. HttpOnlySameSite 속성은 보안을 더욱 강화합니다.
  5. 후속 요청 - 인증된 세션 사용:

    • 이후 사용자는 보호된 리소스를 요청할 때마다 새롭게 할당된 SESSION 쿠키를 사용하여 인증된 상태를 유지할 수 있습니다.
    • 예시 요청:
      GET / HTTP/1.1
      Host: example.com
      Cookie: SESSION=4c66e474-3f5a-43ed-8e48-cc1d8cb1d1c8
    • SESSION 쿠키가 포함되어 있는 이 요청은 서버에서 인증된 상태로 처리되며, 사용자는 추가 인증 절차 없이도 보호된 리소스에 접근할 수 있습니다.

이렇게 함으로써, 사용자는 한 번 로그인한 후 세션이 지속되는 동안 추가적인 인증 없이 보호된 리소스에 접근할 수 있게 됩니다.

이 부분에서는 Spring Security에서 SecurityContextRepository가 사용자의 인증 상태를 이후 요청에서도 유지하는 방식을 설명하고 있습니다. 자세히 설명하겠습니다.

  1. SecurityContextRepository란?

    • Spring Security에서 SecurityContextRepository는 인증된 사용자의 정보를 저장하고, 이후 요청에서 해당 정보를 다시 사용할 수 있도록 도와주는 역할을 합니다.
    • 이를 통해 사용자는 한 번 로그인한 이후에는 매번 인증을 다시 하지 않아도, 세션이 유지되는 동안 보호된 리소스에 접근할 수 있습니다.
  2. DelegatingSecurityContextRepository (위임 보안 컨텍스트 저장소)

    • SecurityContextRepository의 기본 구현체는 DelegatingSecurityContextRepository입니다.
    • 이 저장소는 직접 인증 정보를 저장하지 않고, 이를 두 가지 다른 저장소에 위임하여 처리합니다.
  3. HttpSessionSecurityContextRepository (세션 보안 컨텍스트 저장소)

    • HttpSessionSecurityContextRepository는 인증된 사용자 정보를 HttpSession에 저장합니다.
    • 세션 기반으로 인증 정보를 관리하므로, 사용자가 로그인하고 나서 로그아웃할 때까지는 세션이 유지되고 그 안에 인증 정보가 계속 남아 있습니다.
    • 세션을 이용해 여러 요청 간의 인증 상태를 지속적으로 유지하는데 유용합니다.
  4. RequestAttributeSecurityContextRepository (요청 속성 보안 컨텍스트 저장소)

    • RequestAttributeSecurityContextRepository는 인증 정보를 특정 요청 내에서만 사용하는 방식으로 저장합니다.
    • 요청 속성에 정보를 저장하므로, 요청이 종료되면 인증 정보도 함께 사라지게 됩니다.
    • 따라서 이 방식은 요청 간의 인증 상태를 유지하는 목적보다는 단일 요청 내에서만 인증 정보가 필요할 때 주로 사용됩니다.

이 부분은 Spring Security에서 요청과 관련된 인증 정보를 어떻게 유지할지 결정하는 SecurityContextRepository의 다양한 구현체를 설명하고 있습니다. 각 구현체의 역할을 순서대로 자세히 설명하겠습니다.

1. HttpSessionSecurityContextRepository

  • 역할: HttpSessionSecurityContextRepository는 사용자의 인증 정보를 세션(HttpSession)에 저장합니다.
  • 작동 방식: 사용자가 로그인하면, Spring Security는 사용자의 SecurityContext(즉, 인증된 사용자 정보)를 세션에 저장하여 여러 요청에 걸쳐 인증 상태를 유지할 수 있도록 합니다.
  • 사용 목적: 이 방식은 세션이 유지되는 동안 사용자가 인증된 상태로 남아 있도록 보장합니다. 따라서 사용자가 로그인 후 여러 페이지를 이동할 때마다 인증 정보를 재확인할 필요 없이 지속적인 인증 상태를 제공할 수 있습니다.
  • 대체 가능성: 사용자는 필요에 따라 HttpSessionSecurityContextRepository를 다른 SecurityContextRepository 구현체로 변경하여 다른 방식으로 사용자 인증 상태를 유지할 수도 있습니다.

2. NullSecurityContextRepository

  • 역할: NullSecurityContextRepository는 인증 정보를 저장하지 않으며, 아무 작업도 하지 않는 SecurityContextRepository입니다.
  • 작동 방식: 이 구현체는 SecurityContextHttpSession에 저장하지 않으므로, 인증 상태가 지속되지 않습니다. 사용자 인증이 단일 요청에만 적용되고, 이후 요청에서는 인증 상태가 유지되지 않습니다.
  • 사용 목적: 주로 OAuth 같은 외부 인증을 사용할 때 적합합니다. OAuth 인증은 세션을 통한 인증 상태 유지보다는 매 요청마다 토큰을 확인하는 방식으로 인증을 처리하므로, 세션에 인증 정보를 유지할 필요가 없을 때 사용합니다.

3. RequestAttributeSecurityContextRepository

  • 역할: RequestAttributeSecurityContextRepository는 인증 정보를 요청 속성에 저장합니다. 이는 특정 요청 내에서만 인증 정보를 유지하게 하며, 요청이 끝나면 인증 정보도 사라집니다.
  • 작동 방식: 단일 요청에 대해서만 SecurityContext를 유지하며, 이는 주로 특정 디스패치 흐름(예: 오류 발생 시 재디스패치)에 걸쳐 인증 정보를 유지하고자 할 때 유용합니다.
  • 사용 예시:
    • 예를 들어, 사용자가 인증된 상태에서 요청을 보낸 후 오류가 발생했다고 가정합니다. 일부 서블릿 컨테이너는 오류가 발생하면 SecurityContext를 초기화하고 새로운 오류 디스패치를 수행합니다. 이 경우, 오류 페이지에 도달했을 때는 설정된 인증 정보가 사라져 현재 사용자 정보를 활용할 수 없습니다.
    • RequestAttributeSecurityContextRepository는 이러한 상황에서 오류 디스패치 중에도 SecurityContext가 유지되도록 도와줍니다. 오류 페이지에서도 인증 정보를 참조하여 권한을 부여하거나 사용자 정보를 표시할 수 있게 됩니다.
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        // ...
        .securityContext((securityContext) -> securityContext
            .securityContextRepository(new RequestAttributeSecurityContextRepository())
        );
    return http.build();
}

해석:

이 코드는 Spring Security에서 RequestAttributeSecurityContextRepository를 사용하도록 SecurityFilterChain을 설정하는 예시입니다.

  • filterChain(HttpSecurity http): 이 메서드는 HttpSecurity 객체를 받아 보안 필터 체인을 설정하고, 설정이 완료된 필터 체인을 반환합니다.
  • .securityContext((securityContext) -> securityContext.securityContextRepository(new RequestAttributeSecurityContextRepository())): 이 부분은 SecurityContext를 설정하면서, RequestAttributeSecurityContextRepository를 사용해 인증 정보를 요청 속성에 저장하도록 설정합니다. RequestAttributeSecurityContextRepository를 사용하면 인증 정보가 단일 요청 내에서만 유지됩니다.
  • return http.build();: 설정된 보안 필터 체인을 반환합니다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // ...
        .securityContext((securityContext) -> securityContext
            .securityContextRepository(new DelegatingSecurityContextRepository(
                new RequestAttributeSecurityContextRepository(),
                new HttpSessionSecurityContextRepository()
            ))
        );
    return http.build();
}

해석:

  • DelegatingSecurityContextRepository: DelegatingSecurityContextRepositorySecurityContext를 여러 SecurityContextRepository에 저장하고, 지정된 순서대로 각 저장소에서 인증 정보를 가져올 수 있도록 합니다.
  • 구성 예시: 이 코드는 RequestAttributeSecurityContextRepositoryHttpSessionSecurityContextRepository를 동시에 사용하는 설정 예시입니다.
    • RequestAttributeSecurityContextRepository는 요청 내에서만 인증 정보를 유지하는 반면, HttpSessionSecurityContextRepository는 세션을 통해 인증 상태를 여러 요청에 걸쳐 유지합니다.
  • Spring Security 6의 기본 설정: 이 예시는 Spring Security 6에서 기본 설정으로 사용됩니다.

SecurityContextPersistenceFilter는 Spring Security에서 중요한 역할을 하며, 요청 간에 사용자의 인증 상태(즉, SecurityContext)가 유지되도록 보장하는 필터입니다. 각 단계별로 자세히 설명드리겠습니다.

1. SecurityContext를 로드하여 SecurityContextHolder에 설정

  • SecurityContextPersistenceFilter는 애플리케이션의 나머지 부분이 실행되기 전에 SecurityContextRepository에서 SecurityContext를 로드하여 SecurityContextHolder에 설정합니다.
  • 이렇게 하면 요청이 시작될 때마다 SecurityContextHolder에 인증 정보가 자동으로 설정되어, 이후 애플리케이션의 요청 처리 과정에서 이 인증 정보를 사용할 수 있게 됩니다.

2. 애플리케이션 실행

  • 인증 정보가 SecurityContextHolder에 설정된 후, 애플리케이션의 나머지 로직이 실행됩니다.
  • 이 과정에서는 사용자의 인증 정보를 기반으로 하는 모든 기능(예: 권한 검사, 사용자 정보 접근 등)이 정상적으로 작동할 수 있습니다.

3. SecurityContext 변경 시 SecurityContextRepository에 저장

  • 애플리케이션 로직이 실행된 후, SecurityContextPersistenceFilter는 요청이 종료되기 전에 SecurityContext에 변경이 있었는지 확인합니다.

  • 변경이 확인되면, SecurityContextRepository를 통해 SecurityContext가 저장됩니다. 이렇게 하면 이후 요청에서도 사용자의 인증 상태가 유지됩니다.

    예를 들어, 사용자가 로그인한 후 SecurityContext에 변경이 생겼다면, 이 변경이 SecurityContextRepository에 저장되어 이후 요청에서 다시 사용될 수 있도록 합니다.

문제 상황과 해결 방식

일부 경우에서는 SecurityContextPersistenceFilter가 완료되기 전에 응답이 클라이언트로 전송되어 문제가 발생할 수 있습니다.

  • 예시 1: 리디렉션 응답

    • 클라이언트에게 리디렉션을 보내는 경우 응답이 즉시 작성되므로, 이후 세션 ID를 포함한 SecurityContext를 설정할 수 없습니다.
    • 이미 응답이 클라이언트에게 전송되었기 때문에, 세션에 대한 정보가 제대로 저장되지 않고 누락될 수 있습니다.
  • 예시 2: 빠른 두 번째 요청

    • 클라이언트가 첫 요청에서 인증이 성공했지만, 응답이 완료되기 전에 두 번째 요청을 보낸 경우, 두 번째 요청에서 첫 번째 요청의 인증 정보가 반영되지 않을 수 있습니다. 이로 인해 두 번째 요청에서는 잘못된 인증 상태가 적용될 수 있습니다.

해결책: HttpServletRequest와 HttpServletResponse 래핑

  • 이러한 문제를 해결하기 위해, SecurityContextPersistenceFilterHttpServletRequestHttpServletResponse를 모두 래핑합니다.
  • 이를 통해 응답이 클라이언트에 전송되기 전에 SecurityContext가 변경되었는지 여부를 감지하고, 변경이 확인되면 응답이 커밋되기 직전에 SecurityContext를 저장합니다.
  • 이렇게 하면 요청 간에 인증 상태가 일관되게 유지되며, 응답이 커밋되기 전에 모든 보안 상태가 완전히 설정되도록 보장할 수 있습니다.

요약: SecurityContextPersistenceFilterSecurityContextRepository와 함께 요청 간 인증 정보를 안전하게 유지하고, 불완전한 응답 전송으로 인한 인증 정보 손실을 방지합니다.


SecurityContextHolderFilterSecurityContextPersistenceFilter와 비슷한 역할을 하지만 몇 가지 중요한 차이점이 있습니다. 이 차이와 SecurityContextHolderFilter의 동작 방식을 순서대로 설명드리겠습니다.

SecurityContextHolderFilter의 주요 기능

  • 기본 역할: SecurityContextHolderFilterSecurityContextRepository에서 인증 정보(SecurityContext)를 불러와 SecurityContextHolder에 설정합니다.
  • 단, 저장 기능은 없음: SecurityContextHolderFilter는 요청마다 SecurityContext를 로드하지만, 이 필터는 요청이 끝난 후 인증 상태를 저장하지 않습니다. 따라서 인증 정보를 유지하려면, 인증 정보를 명시적으로 저장하는 추가 작업이 필요합니다.

동작 방식

SecurityContextHolderFilter의 동작 방식은 크게 세 단계로 요약할 수 있습니다:

  1. SecurityContext 로드: 애플리케이션의 나머지 부분이 실행되기 전에 SecurityContextHolderFilterSecurityContextRepository에서 SecurityContext를 로드하여 SecurityContextHolder에 설정합니다.
  2. 애플리케이션 실행: 로드된 SecurityContext 정보가 SecurityContextHolder에 설정된 상태에서 애플리케이션 로직이 실행됩니다.
  3. SecurityContext 저장 필요: SecurityContextPersistenceFilter와 달리, SecurityContextHolderFilter는 변경된 SecurityContext를 자동으로 저장하지 않기 때문에, 필요한 경우 개발자가 이를 명시적으로 저장해야 합니다.

SecurityContext 명시적 저장 설정

이 필터를 사용할 때는 SecurityContext가 변경되면 반드시 이를 명시적으로 저장해야 합니다. Spring Security 설정에서는 다음과 같이 .requireExplicitSave(true) 옵션을 사용하여 명시적 저장을 요구하도록 설정할 수 있습니다:

public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .securityContext((securityContext) -> securityContext
            .requireExplicitSave(true)
        );
    return http.build();
}

이 설정은 SecurityContextHolderSecurityContext를 설정하는 코드가 있을 때, SecurityContextRepository에 이 정보를 명시적으로 저장하도록 강제합니다.

명시적 저장 예시

SecurityContextHolderFilter를 사용할 때는 SecurityContextHolderSecurityContext를 설정한 후, SecurityContextRepository를 통해 SecurityContext를 명시적으로 저장해야 합니다.

  • 잘못된 예시:

    SecurityContextHolder.setContext(securityContext);

    이 코드는 SecurityContextHoldersecurityContext를 설정하지만, SecurityContext가 이후 요청에 자동으로 저장되지 않기 때문에 상태가 유지되지 않습니다.

  • 올바른 예시:

    SecurityContextHolder.setContext(securityContext);
    securityContextRepository.saveContext(securityContext, httpServletRequest, httpServletResponse);

    여기서는 SecurityContextHoldersecurityContext를 설정한 뒤, securityContextRepository.saveContext 메서드를 호출하여 인증 정보를 명시적으로 저장합니다. 이렇게 해야 이후 요청에서도 인증 상태가 일관되게 유지됩니다.

요약

  • SecurityContextHolderFilterSecurityContext를 로드하는 역할만 하며, 자동으로 저장하지 않기 때문에 명시적으로 저장해야 합니다.
  • .requireExplicitSave(true) 옵션을 사용하면, SecurityContextHolderSecurityContext를 설정하는 경우 반드시 SecurityContextRepository를 통해 저장하도록 강제됩니다.
  • 이렇게 명시적으로 저장해야 하는 이유는 요청 간의 일관된 인증 상태를 유지하고, 인증 정보가 의도치 않게 사라지는 것을 방지하기 위해서입니다.

0개의 댓글