JWT에서 Session으로의 전환

하루히즘·2022년 2월 2일
1

Spring Framework

목록 보기
14/15
post-custom-banner

서론

예전에 작성했던 포스트에 달린 댓글에서 고민했던 부분은 지난 기술 면접을 통해 JWT를 걷어내고 세션으로 돌아가자고 결정하게 되었다. 그러면서 경험한 내용을 적어보고자 한다.

본론

어떻게 저장할 것인가?

JWT 대신 세션을 사용하려는 이유는 이전 포스트에서 여러번 언급했으니 더 이상 적을 필요는 없을 것 같은데 세션을 어떻게 분산 환경에서 관리할 것인가에 대해서는 생각해 볼 필요가 있었다.

일단 현재 애플리케이션(SimpleTodoList)은 Heroku에 단일 인스턴스로 배포되어 있다. 하지만 추후 Docker 이미지로 묶어서 로컬 가상 머신이나 AWS EC2 인스턴스같은 곳에 직접 배포해보면서 CI/CD 프로세스에 활용해보려고 하고 있기 때문에 확장성을 고려해야 했다.

그러면서 내가 애용하는 스프링 생태계 프로젝트 내에서 해결할 수 있는 방법을 우선으로 찾았는데 다행히 Spring Session이라는 이름부터 세션을 다루기에 적합할 것 같은 프로젝트가 제공되고 있었다.

이론상으로는 Sticky Session, Session Clustering 등 다양한 방법이 있지만 장단점을 비교해봤을 때 별도의 세션 저장소를 만들어두는 게 좋다고 생각했으며 해당 저장소로 빠르게 접근할 수 있는 키-값 NoSQL 데이터베이스인 Redis를 선택했다.

Spring Session 적용

Spring Session 프로젝트의 설명을 읽어보면 다음과 같다.

Spring Session provides an API and implementations for managing a user’s session information.

Spring Session makes it trivial to support clustered sessions without being tied to an application container specific solution.

즉 사용자의 세션을 관리하는 API와 그 구현을 제공하여 특정 애플리케이션 컨테이너(톰캣 등)에 종속되지 않고 클러스터된 세션을 쉽게 관리할 수 있도록 도와주는 프로젝트인 것을 알 수 있다.

윗 문단에서 언급했듯이 애플리케이션을 여러 Docker 컨테이너로 구성할 것이기 때문에 여러 웹 서버간 세션 클러스터링이 필요하며 그 중에서도 Redis를 통해 세션을 관리할 수 있는 Spring Session Data Redis가 제공된다.

이런 Spring Session의 좋은 점은 기존 스프링 애플리케이션에서 사용하던 HttpSession의 대체 구현체를 제공하는데 언급했듯이 특정 애플리케이션 컨테이너(스프링 애플리케이션이라면 대개 아파치 톰캣)에 종속적이지 않고 RESTful API 통신에 활용하는 경우를 위해 쿠키가 아닌 헤더 기반 세션ID 기능도 지원한다는 것이다.

이 과정은 서블릿 필터를 통해 이루어지는데 SessionRepositoryFilter라는 필터를 springSessionRepositoryFilter라는 이름으로 등록하여 HttpSession을 Spring Session에서 제공하는 별도의 Session 인터페이스로 변환한다. 그리고 HTTP 요청의 쿠키(기본 설정)나 헤더에서 세션 ID를 추출하여 세션 저장소에서 꺼내서 세션을 복구하게 된다.

이런 세션 데이터가 저장되어 있는 세션 저장소는 SessionRepository라는 인터페이스로 제공된다. Spring Session 프로젝트에서는 Redis나 MongoDB, Hazelcast 등을 제공하고 있으며 애플리케이션 설정에서 등록한 저장소에 따라 구현체가 제공된다. 당연하지만 Spring Boot의 경우 application properties나 yml에 저장소만 정해주면 위의 과정이 자동 설정으로 진행된다.

spring:
  session:
    store-type: redis

위의 경우 Redis 데이터베이스를 이용하여 세션을 관리하도록 설정하였으며 이후 세션을 발급하면 아래처럼 식별자가 데이터베이스에 저장되는 것을 볼 수 있다.

1) "spring:session:expirations:1643790660000"
2) "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:kwonkyu"
3) "backup1"
4) "spring:session:sessions:94a96d7f-3303-4e0d-80fd-b9b289283f99"
5) "backup4"
6) "spring:session:sessions:expires:94a96d7f-3303-4e0d-80fd-b9b289283f99"
7) "backup2"
8) "backup3"
127.0.0.1:6379>

Spring Security와 통합

Spring Session은 Spring Security의 Security Context와도 같이 동작할 수 있는데 역시 별도의 설정이 필요하지 않다. 대신 HttpSessionSecurityContextRepository에서 자동으로 세션에서 시큐리티 컨텍스트를 불러와서 현재 컨텍스트에 적용해주기 때문에 이전에 인증된 상태를 복구할 수 있다.

중요한 것은 세션에서 시큐리티 컨텍스트를 불러오기 위해 세션을 먼저 활성화시켜야 하는데 이를 위해서 Spring Session의 SessionRepositoryFilter가 Spring Security의 필터 체인보다 먼저 위치해야 한다. 물론 자동 설정에서는 별도로 건드릴 필요가 없다.

그리고 이전에 JWT를 적용했을 때는 사용하지 않았던 Spring Security의 로그아웃 기능도 이번에 활용하면서 좀 더 올바른 로그인, 로그아웃 기능을 구현할 수 있었다.

결론

생각보다 Spring Session을 적용하는 과정은 간단했고 오히려 기존의 JWT 인증 방식을 활용한 JwtAuthentication에서 UsernamePasswordAuthenticationToken을 이용한 UserDetails로 인증 객체를 변환하다보니 @AuthenticationPrincipal으로 잘못 주입받아서 NPE 때문에 많은 시간을 낭비하게 되었다. 앞으로는 스프링에서 제공하는 기능을 사용할 때 좀 더 신중히 사용해야 하지 않을까 싶다.

추가 적용사항으로는 remember-me 서비스를 어떻게 Spring Session과 같이 활용할 수 있을지 알아보려고 한다. 프로젝트에도 이미 SpringSessionRememberMeServices라는 클래스가 있기 때문에 통합은 어렵지 않을 것 같지만 Spring Security에서 제공하는 remember-me 서비스는 제대로 활용해 본 적이 없다. 기본적으로 In-Memory 저장소에 remember-me 토큰을 저장한다고 하니 이것도 위의 클래스를 활용해서 Redis에 같이 넣어서 보관하는 식으로 해야하지 않을까 싶다.

profile
YUKI.N > READY?
post-custom-banner

0개의 댓글