톰캣에서 기본적으로 메모리에 세션을 저장하고 관리하기 때문에 서버를 재실행하면 사용자의 세션이 사라져 재인증을 받아야 하는 상황이 생긴다.
그래서 보통 세션을 In-memory DB와 Disk based DB 중 하나를 선택해서 저장을 한다.
로컬에서는 MySQL을 사용하고 있었기 때문에 빠르게 세팅할 수 있는 JDBC를 선택하였다.
물론 운영할 때는 redis로 변경하는 것이 속도 측면에서 좋아 보인다.
implementation 'org.springframework.session:spring-session-jdbc'
위 라이브러리를 의존받으면 정말 간단하게 @EnableJdbcHttpSession 어노테이션으로 쉽게 세션을 DB에 자동으로 관리하고 저장할 수 있게 된다.
자세한 설정은 아래 사이트를 참고하자.
https://docs.spring.io/spring-session/reference/guides/boot-jdbc.html#httpsession-jdbc-boot-spring-configuration
톰캣에서는 org.apache.catalina.session 패키지의 ManagerBase.class 에서 세션을 생성한다고 한다.
spring-session-jdbc에서는 SessionRepositoryFilter 라는 클래스를 Servlet Filter Chain에 등록하는데 세션을 톰캣보다 먼저 처리하기 위해 다음과 같이
@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public static final int DEFAULT_ORDER = Integer.MIN_VALUE + 50;
처리 우선 순위를 가장 높게 줘서 필터를 등록한 걸 볼 수 있다.
그리고 SessionRepositoryFilter 는 SecurityContextHolder 와 연관되어 있다.
SecurityContextHolder 는 ThreadLocal에서 인증 객체를 관리해 주는 용도이다.
이게 왜 연관성이 있냐면 공식 문서에서도 설명이 나와 있는데 spring-session-jdbc와 SecurityContextPersistenceFilter는 사용자 인증 정보를 HttpSession에 저장할 때 이 정보가 DB까지 영속화된다고 나와 있다.
그러면 세션이 어떻게 저장되는지 클라이언트의 요청과 응답을 통해 간단하게 알아보도록 하자.
만약 로그인하려는 사용자가 API를 요청했다고 하자.
- 사용자 정보를 확인하고 인증된 사용자의 Authentication 구현체를 SecurityContextHolder에 저장한다.
- 응답을 주기 위해 거쳐왔던 Filter 중 Security Filter Chain에 속한 SecurityContextPersistenceFilter(세션 구현체는 HttpSessionSecurityContextRepository) 라는 필터에서 SecurityContextHolder에 저장된 Contenxt를 받아 세션에 사용자 정보를 저장한다.
그리고 저장된 Context는 모두 비워준다.- Servlet Filter Chain에 속한 SessionRepositoryFilter 에서 저장된 세션 정보를 DB에 저장한다.
다음과 같은 단계로 세션이 저장되는 것을 알 수 있다.
사용자가 API를 요청했다고 하자.
- SessionRepositoryFilter 에서 세션이 있다면 SESSION_ATTRIBUTES 테이블에 사용자 정보까지 조회한다. 세션 속성에 그 정보를 저장하고 비즈니스 로직을 모두 수행한 후 응답 되기 전에 세션 정보를 DB에 저장한다.
만약 세션이 없다면 익명의 세션을 생성하고 비즈니스 로직을 모두 수행 후 DB에 저장한다.- SecurityContextPersistenceFilter 에서 세션이 존재하다면 세션 속성에서 사용자 정보를 추출하여 SecurityContextHolder에 저장한다.
위와 같은 단계로 세션을 처리하는데 이후 컨트롤러에서 인증된 사용자는 SecurityContextHolder에 Context가 존재하니 @AuthenticationPrincipal 어노테이션을 사용하여 인증 객체를 매개변수로 받을 수 있게 된다.
요청 흐름 1단계를 보면 쿠키에 "SESSION"이라는 키 값이 없다면 새로운 세션을 생성한다고 하였다.
만약 세션이 없는 사용자가 API를 요청하면
위 이미지처럼 DB에 익명 세션이 생성된다.
DB에 무분별한 익명 세션 저장을 막기 위해 다음과 같이 해결하였다.
OncePerRequestFilter 를 상속받은 커스텀 필터 클래스를 만들고 Security Filter Chain에 속한 AnonymousAuthenticationFilter 이전에 필터를 등록한다.
커스텀 필터를 저 필터 이전에 등록한 이유는 저기서 인증 객체를 만들어주기 때문에 필터를 모두 순회하고 돌아올 때 세션을 조회하면 익명의 세션을 받을 수 있기 때문이다.
그래서 익명 세션을 무효화 시켜준다면 SessionRepositoryFilter 에서 세션 정보를 가지고 DB에 저장하는데 세션이 무효화되었기 때문에 DB에 저장이 안 된다.
그렇다면 익명 세션이라는 것을 어떻게 알 수 있을까?
디버깅해서 확인해 본 결과 익명 객체에 대한 세션의 속성 키값에는 "SPRING_SECURITY_SAVED_REQUEST" 가 저장돼있고
인증 객체에 대한 세션의 속성 키 값은 "SPRING_SECURITY_CONTEXT" 로 저장되어 있다.
그래서 위와 같은 코드로 익명의 객체를 판단할 수 있게 된다.
https://semtax.tistory.com/92
https://yousrain.tistory.com/13
https://okky.kr/questions/259809
http://arahansa.github.io/docs_spring/session.html#api-session
https://docs.spring.io/spring-session/reference/guides/boot-jdbc.html