요청이 인증되도록 설정한 애플리케이션을 확보했다면, 그 결과로 생성된 인증 정보를 어떻게 지속적으로 유지하고 이후 요청에서 복원할지를 고려하는 것이 중요합니다.
이 작업은 기본적으로 자동으로 처리되기 때문에 추가적인 코드가 필요하지 않지만, HttpSecurity에서 requireExplicitSave가 무엇을 의미하는지 알고 있는 것이 중요합니다.
원하면 requireExplicitSave가 무엇을 수행하는지 또는 왜 중요한지에 대해 더 읽을 수 있습니다. 그렇지 않으면 대부분의 경우 이 섹션에서 할 일은 끝났습니다.
그러나 이 기능 중 일부가 애플리케이션에 필요한지 고려해 보세요:
세션 관리 구성 요소 이해하기
세션 관리 기능은 몇 가지 구성 요소로 구성되어 함께 동작하여 기능을 제공합니다. 이러한 구성 요소에는 SecurityContextHolderFilter, SecurityContextPersistenceFilter, 그리고 SessionManagementFilter가 있습니다.
Spring Security 6에서는 기본적으로 SecurityContextPersistenceFilter와 SessionManagementFilter가 설정되지 않습니다. 또한, 애플리케이션은 SecurityContextHolderFilter 또는 SecurityContextPersistenceFilter 중 하나만 설정해야 하며, 둘을 동시에 설정해서는 안 됩니다.
Spring Security의 SessionManagementFilter는 사용자의 인증 상태를 확인하고 관리하는 중요한 역할을 합니다. 이 필터는 주로 세션 기반 인증과 관련된 다양한 시나리오에서 인증 상태를 관리하고 적절한 처리를 수행합니다. 아래에 SessionManagementFilter의 동작 원리를 더 자세히 설명하겠습니다.
SessionManagementFilter는 SecurityContextRepository에 저장된 보안 컨텍스트와 현재 요청의 SecurityContextHolder를 비교하여 사용자가 인증되었는지를 확인합니다.SessionManagementFilter는 사용자가 인증되었는지를 판단할 때, 비대화식 인증 방식(non-interactive authentication mechanism)을 고려합니다. 비대화식 인증 방식에는 선행 인증(pre-authentication)이나 remember-me 기능이 포함됩니다.remember-me 기능을 활성화하고 로그인한 상태라면, 새 요청에서도 추가적인 로그인 절차 없이 인증 상태가 유지될 수 있습니다. SessionManagementFilter는 이런 경우를 감지하여 인증 상태를 유지시킵니다.SecurityContextRepository에 보안 컨텍스트가 존재하면, 필터는 더 이상 작업을 수행하지 않고 그대로 요청을 진행시킵니다.SecurityContextRepository에 보안 컨텍스트가 없지만, SecurityContextHolder에 인증 객체(Authentication)가 포함되어 있으면, 이는 이전에 인증된 것으로 간주합니다. 이 경우, SessionManagementFilter는 사용자가 이전 필터에서 인증된 것으로 판단하고, 설정된 SessionAuthenticationStrategy를 호출하여 인증된 상태로 처리합니다.SessionAuthenticationStrategy는 세션 기반 인증 전략을 정의합니다. 사용자가 성공적으로 인증되었을 때, 이 전략을 통해 추가적인 세션 관리 작업을 수행할 수 있습니다.SessionAuthenticationStrategy에서 이러한 제어를 할 수 있습니다.SessionManagementFilter는 요청된 세션 ID가 유효한지 여부를 확인합니다. 만약 잘못된 세션 ID가 요청되었거나 세션이 타임아웃된 경우, 설정된 InvalidSessionStrategy를 호출합니다.SimpleRedirectInvalidSessionStrategy가 담당합니다. 예를 들어, 세션이 만료된 경우 로그인 페이지로 리다이렉트하여 사용자가 다시 로그인할 수 있도록 할 수 있습니다.SimpleRedirectInvalidSessionStrategy는 InvalidSessionStrategy의 한 구현체로, 잘못된 세션이 감지되었을 때 리다이렉트 URL을 설정하는 간단한 방식입니다.SessionManagementFilter는 다음과 같은 순서로 동작합니다:
1. SecurityContextRepository와 SecurityContextHolder를 비교하여 사용자가 인증된 상태인지 확인합니다.
2. 보안 컨텍스트가 있는 경우: 아무 작업 없이 그대로 요청을 진행시킵니다.
3. 보안 컨텍스트가 없는 경우:
SecurityContextHolder에 인증 객체가 있으면, 인증된 상태로 간주하고 SessionAuthenticationStrategy를 호출하여 세션 관리 작업을 수행합니다.InvalidSessionStrategy를 호출합니다.InvalidSessionStrategy는 유효하지 않은 세션이 발생했을 때 사용자에게 리다이렉트 URL을 제공하거나 다른 방식으로 처리합니다.이와 같은 과정을 통해 SessionManagementFilter는 세션의 유효성을 확인하고, 사용자 인증 상태를 적절하게 관리하여 보안 수준을 유지합니다.
Spring Security 5와 Spring Security 6에서의 SessionManagementFilter 사용 방식과 그 차이점을 자세히 설명해 드리겠습니다.
SessionManagementFilter가 기본적으로 설정되어 있어, 사용자가 인증되었는지 여부를 확인하고 인증이 성공한 후의 추가 처리를 담당합니다.SessionAuthenticationStrategy를 호출하여, 사용자가 인증된 후 세션 관리 작업(예: 세션 고정 방지, 동시 세션 수 제한 등)을 수행하게 합니다.SessionManagementFilter가 동작하기 위해서는 매 요청마다 HttpSession을 확인해야 합니다. 즉, 요청이 들어올 때마다 HttpSession이 존재하는지 읽고, 보안 컨텍스트에서 인증 상태를 확인해야 합니다.SessionManagementFilter가 기본 설정에서 제외되었습니다. 대신, 인증 메커니즘 자체가 SessionAuthenticationStrategy를 호출하는 방식으로 변경되었습니다.SessionAuthenticationStrategy가 호출되므로, 매 요청마다 HttpSession을 읽을 필요가 없어졌습니다.SessionManagementFilter가 모든 요청에서 사용자의 인증 상태를 확인하고, 인증이 완료된 후 SessionAuthenticationStrategy를 호출하여 세션 관리 작업을 수행합니다.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에서는 SecurityContextPersistenceFilter가 기본적으로 사용됩니다. 이 필터는 요청이 처리되는 동안 보안 컨텍스트를 관리하며, SecurityContext를 자동으로 SecurityContextRepository에 저장합니다.
자동 저장 동작:
HttpServletResponse가 커밋되기 직전, 즉 요청이 마무리될 때 자동으로 저장됩니다.문제점:
Spring Security 6에서는 이러한 문제를 해결하기 위해 SecurityContextPersistenceFilter가 SecurityContextHolderFilter로 대체되었으며, Require Explicit Save 방식이 도입되었습니다.
명시적 저장 방식:
SecurityContextHolderFilter는 요청을 처리할 때 SecurityContextRepository에서 SecurityContext를 읽어와 SecurityContextHolder에만 채워 넣는 역할만 합니다.명시적 저장의 장점:
다음 예제는 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);
}
보안 컨텍스트 생성 및 인증 정보 설정:
SecurityContextHolder.createEmptyContext()를 통해 새로운 보안 컨텍스트를 생성합니다.SecurityContextRepository에 명시적 저장:
securityContextRepository.saveContext(context, request, response); 코드를 통해 보안 컨텍스트를 SecurityContextRepository(HttpSession 등)에 명시적으로 저장합니다.SecurityContextHolderFilter가 SecurityContext를 읽기만 하고, 명시적으로 saveContext 메서드를 호출해야 저장됩니다.Require Explicit Save 설정은 성능을 최적화하고, 보안 컨텍스트의 저장 시점을 명확하게 제어할 수 있도록 하여 Spring Security 애플리케이션을 더 효율적으로 관리할 수 있게 합니다.
요약하자면, requireExplicitSave가 true일 때 Spring Security는 SecurityContextPersistenceFilter 대신 SecurityContextHolderFilter를 설정합니다.
세션은 자체적으로 만료되며, 보안 컨텍스트를 제거하기 위해 추가 작업을 수행할 필요는 없습니다. 그러나 Spring Security는 세션이 만료되었을 때 이를 감지하고 특정 동작을 수행하도록 설정할 수 있습니다. 예를 들어, 사용자가 만료된 세션으로 요청을 보낼 경우 특정 엔드포인트로 리다이렉트하도록 설정할 수 있습니다. 이는 HttpSecurity의 invalidSessionUrl을 통해 설정됩니다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.invalidSessionUrl("/invalidSession")
);
return http.build();
}
세션 타임아웃 감지 시 오탐 가능성:
해결 방법:
invalidSessionUrl을 사용하면 세션이 만료되었을 때 사용자에게 명확한 안내를 제공할 수 있어, 사용자 경험을 개선할 수 있습니다.
로그아웃 시 JSESSIONID 쿠키를 명시적으로 삭제할 수 있습니다. 예를 들어, 로그아웃 핸들러에서 Clear-Site-Data 헤더를 사용하여 쿠키를 삭제할 수 있습니다:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.logout((logout) -> logout
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(COOKIES)))
);
return http.build();
}
로그아웃 핸들러에서 다른 방식으로도 세션 쿠키를 삭제할 수 있습니다. 이후 코드 예시를 참고하여 필요한 방식으로 구현할 수 있습니다.
Spring Security에서 로그아웃 시 세션 쿠키(JSESSIONID)를 삭제하는 방법에 대해 자세히 설명드리겠습니다.
Spring Security는 deleteCookies() 메서드를 통해 로그아웃 시 특정 쿠키를 삭제하도록 설정할 수 있습니다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.logout(logout -> logout
.deleteCookies("JSESSIONID") // JSESSIONID 쿠키 삭제 설정
);
return http.build();
}
deleteCookies("JSESSIONID"): 로그아웃 요청이 처리된 후, JSESSIONID 쿠키를 삭제합니다.만약 애플리케이션이 프록시 뒤에서 실행되고 있다면, 프록시 서버를 구성하여 쿠키를 삭제할 수도 있습니다.
다음은 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:Set-Cookie 값을 추가합니다.JSESSIONID=;: 쿠키 값을 빈 값으로 설정합니다.Path=/tutorial;: 쿠키의 유효 경로를 애플리케이션의 경로(/tutorial)로 제한합니다.Expires=Thu, 01 Jan 1970 00:00:00 GMT;: 만료일을 과거로 설정하여 쿠키를 즉시 만료시킵니다.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:COOKIES: 브라우저 쿠키를 삭제합니다.| 방법 | 장점 | 제한사항 |
|---|---|---|
deleteCookies 메서드 | 코드 수준에서 간단히 설정 가능 | 모든 서블릿 컨테이너에서 동작을 보장할 수 없으므로 테스트 필요 |
| 프록시 서버(Apache HTTPD) | 프록시 레벨에서 제어하므로 애플리케이션 코드 수정 불필요 | 프록시 설정에 대한 추가 작업 필요, 프록시가 없는 경우 사용할 수 없음 |
| Clear-Site-Data 헤더 | 컨테이너 독립적이며, 쿠키 외에도 캐시 및 스토리지 삭제 가능 | Clear-Site-Data 헤더를 지원하지 않는 브라우저나 환경에서는 작동하지 않을 수 있음 |
deleteCookies("JSESSIONID")를 사용하여 Spring Security 수준에서 해결.각 환경에 맞는 방법을 선택하여 구현하면, 로그아웃 시 세션 쿠키 삭제를 안전하고 효율적으로 처리할 수 있습니다.
세션 고정 공격(Session Fixation Attack)은 악의적인 공격자가 사이트에 접근하여 세션을 생성한 뒤, 다른 사용자에게 동일한 세션을 사용하도록 유도하는 방식으로 이루어질 수 있습니다. 예를 들어, 공격자는 세션 식별자를 URL 파라미터로 포함한 링크를 피해자에게 보내 로그인하도록 유도할 수 있습니다.
Spring Security는 사용자가 로그인할 때 새로운 세션을 생성하거나 세션 ID를 변경하여 이러한 공격을 자동으로 방지합니다.
Spring Security에서 세션 고정 보호를 위한 전략은 다음 세 가지로 설정할 수 있습니다:
changeSessionId (기본값)HttpServletRequest#changeSessionId() 메서드를 사용하여 구현됩니다.newSessionmigrateSession@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement((session) -> session
.sessionFixation((sessionFixation) -> sessionFixation
.newSession() // 새로운 세션을 생성
)
);
return http.build();
}
newSession(): 로그인이 이루어질 때 기존 세션을 사용하지 않고 새 세션을 생성합니다.SessionFixationProtectionEvent가 발행됩니다.changeSessionId 사용 시:jakarta.servlet.http.HttpSessionIdListener도 함께 알림을 받습니다. 따라서 두 이벤트를 동시에 수신하도록 설정된 경우, 적절히 관리해야 합니다..sessionFixation((sessionFixation) -> sessionFixation.none())
changeSessionId 또는 migrateSession으로 설정됩니다.다음 코드는 SecurityContextHolder를 사용하여 보안 컨텍스트를 설정하는 방법을 보여줍니다:
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(), loginRequest.getPassword());
Authentication authentication = this.authenticationManager.authenticate(token);
// ...
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
SecurityContext의 빈 인스턴스 생성:
SecurityContextHolder.createEmptyContext()를 호출하여 빈 SecurityContext 인스턴스를 생성합니다.Authentication 객체 설정:
context.setAuthentication(authentication)를 통해 인증된 Authentication 객체를 SecurityContext에 설정합니다.SecurityContext를 SecurityContextHolder에 설정:
SecurityContextHolder.setContext(context)를 호출하여 SecurityContext를 SecurityContextHolder에 저장합니다.SecurityContextHolder는 정적(static)으로 관리되며, 애플리케이션의 현재 스레드에 보안 컨텍스트를 저장합니다.SecurityContextHolder를 정적으로 호출하여 빈 SecurityContext를 생성합니다.SecurityContext에 인증 객체를 설정합니다.SecurityContext를 SecurityContextHolder에 저장하여 현재 요청에서 사용 가능하게 합니다.Spring Security에서 SecurityContextHolder와 SecurityContextHolderStrategy를 다루는 방식에 대해 더 깊이 설명하겠습니다. 특히, 기존 코드의 문제점과 이를 해결하기 위해 SecurityContextHolderStrategy를 주입받아 사용하는 방법을 자세히 다뤄보겠습니다.
SecurityContextHolder와 SecurityContextHolderStrategy 기본 개념SecurityContextHolderSecurityContext 객체를 스레드 로컬(Thread Local)에 저장하여, 현재 스레드에서 사용되는 인증 정보에 접근할 수 있습니다.SecurityContextHolder를 통해 빈 SecurityContext를 생성.SecurityContext에 Authentication 객체를 설정.SecurityContext를 다시 SecurityContextHolder에 저장.SecurityContextHolderStrategySecurityContextHolder의 동작 방식을 결정하는 전략 객체입니다.SecurityContextHolderStrategy를 설정합니다.ThreadLocalSecurityContextHolderStrategy를 사용하여 인증 정보를 스레드 로컬에 저장합니다.SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
정적 접근으로 인한 경합:
SecurityContextHolder는 클래스 로더당 하나의 SecurityContextHolderStrategy를 사용하므로, 여러 애플리케이션 컨텍스트가 같은 전략을 공유합니다.전역 상태 관리의 비효율성:
SecurityContextHolder에 직접 접근하면, 애플리케이션 테스트나 멀티 컨텍스트 환경에서 의도하지 않은 동작이 발생할 수 있습니다.유연성 부족:
SecurityContextHolder를 사용하는 방식은 전략 변경이나 커스터마이징에 제약이 있습니다.SecurityContextHolderStrategy를 사용해야 하는 경우 설정이 어렵습니다.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 저장
}
}
SecurityContextHolderStrategy를 주입받아 사용:
SecurityContextHolder.getContextHolderStrategy() 메서드를 호출하여 현재 애플리케이션 컨텍스트에서 사용할 SecurityContextHolderStrategy를 가져옵니다.SecurityContextHolderStrategy를 독립적으로 사용할 수 있습니다.SecurityContext 생성 및 설정:
securityContextHolderStrategy.createEmptyContext()를 호출하여 빈 SecurityContext를 생성합니다.Authentication 객체를 설정합니다.SecurityContext 저장:
securityContextHolderStrategy.setContext(context)를 호출하여 SecurityContext를 설정합니다.SecurityContextHolderStrategy를 통해 저장하므로, 클래스 로더 충돌 없이 안전하게 사용할 수 있습니다.컨텍스트 간 독립성:
SecurityContextHolderStrategy를 사용하므로, 경합 조건(race condition)이 제거됩니다.유연성 증가:
SecurityContextHolderStrategy를 커스터마이징하거나 교체할 수 있으므로, 테스트와 확장성이 좋아집니다.전역 상태 의존성 제거:
테스트 용이성:
SecurityContextHolderStrategy를 주입받아 사용할 수 있어, 테스트 결과의 신뢰도가 높아집니다.기존 방식의 문제를 해결하기 위해:
SecurityContextHolder의 정적 접근을 피하고, SecurityContextHolderStrategy를 사용합니다.때로는 세션을 즉시 생성(eager session creation)하는 것이 유용할 수 있습니다. Spring Security에서는 이를 위해 ForceEagerSessionCreationFilter를 사용할 수 있으며, 다음과 같이 구성할 수 있습니다:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS) // 세션을 항상 즉시 생성
);
return http.build();
}
SessionCreationPolicy.ALWAYS: