[Spring Boot] JWT 토큰 관리 - 블랙리스트 (feat. Redis)

김찬미·2024년 7월 8일
0

Spring Boot

목록 보기
2/7
post-thumbnail

🤔 JWT의 단점, 로그아웃 구현

JWT를 프로젝트에 도입하게 되면 처음 마주치는 장애물이 있다. 바로 로그아웃이다. 일반 세션 기반 인증과 달리 JWT는 발급 후에 서버에서 정보를 저장하지 않는 stateless 특성을 가지고 있기 때문에, 클라이언트가 발급받은 JWT는 만료될 때까지 계속 유효하다.

그렇다면 JWT에서는 어떻게 로그아웃 기능을 구현할까? 일반적으로는 Redis를 사용한 블랙리스트 기법을 가장 많이 사용한다.

🚫 블랙리스트(Blacklist)란?

블랙리스트는 이러한 JWT의 단점을 보안하기 위해 고안된 기법으로, 사용자가 로그아웃을 하는 시점에서 데이터베이스에 토큰과 유효기간을 추가함으로 해당 토큰을 사용하여 인증된 요청이 거부되도록 한다.

JWT 토큰을 블랙리스트에 추가하려면 주로 Redis와 같은 인메모리 데이터베이스를 사용한다. 특히 Redis는 빠른 속도와 메모리 기반 데이터 저장 방식을 갖고 있어 블랙리스트에 최적화되어 있다고 할 수 있다.

🤔 인메모리 데이터베이스란?
데이터를 영구 저장 매체가 아닌 메모리에 저장하고 관리하는 데이터베이스이다. 데이터 접근 속도가 매우 빠르지만, 전원이 꺼지면 데이터가 사라지는 휘발성이라는 특성때문에 주로 임시 데이터나 캐시 데이터를 저장한다.

✅ 블랙리스트 동작 원리

  • 토큰 추가: 토큰의 고유 식별자를 키로 하여 Redis에 저장한다. 또한 RedisTTL(Time To Live) 특성을 활용해 만료 시간을 설정하여 일정 기간 후에 자동으로 삭제되도록 한다.

  • 토큰 확인: 요청이 들어올 때마다 해당 JWT 토큰이 블랙리스트에 있는지 확인한다. Redis에서 해당 키를 조회하여 토큰의 존재 여부를 판단하고, 존재할 경우 요청을 거부한다.


어디까지 Stateful를 허용해야 할까?

JWT는 기본적으로 Stateless하다. 그러나 로그아웃 기능을 제대로 구현하려면 필연적으로 Stateful한 요소를 넣을 수 밖에 없다. 그렇다면 블랙 리스트에서 어떤 방식을 선택해야 사용자 경험과 Stateless 사이 균형을 맞출 수 있을까?

1) if 블랙리스트에 AccessToken 저장?

만약 로그아웃 시 AccessToken만 저장한다면 치명적인 문제점이 생긴다. 아직 남아있는 RefreshToken으로 새로운 AccessToken을 발급받으면 그만이기 때문이다.

많은 레퍼런스에서 블랙리스트 기법을 소개할 때 AccessToken을 저장했지만, 나는 아무리 생각해도 이 점이 걸려서 AccessToken을 저장하지 않기로 했다. 애초에 지금 프로젝트에서는 AccessToken의 유효기간을 매우 짧게 설정해놨기에 보안적으로도 크게 문제가 없기 때문이다.

2) if 블랙리스트에 RefreshToken 저장? ✔️

내가 선택한 방법이다.

일단 유효기간이 긴 RefreshToken을 탈취당하는 상황은 매우 최악이기에 이것을 방지하려는 목적도 있었고, 만약 RefreshToken을 블랙리스트에 저장한다면 AccessToken을 재발급받는 시점에서 유효성을 검사할 수 있기 때문에 훨씬 합리적이다.

이 방법을 쓸 경우 사용자가 로그아웃할 경우에 프론트엔드에서 직접 AccessToken을 삭제하면 된다. 만약 AccessToken이 탈취되었더라도 RefreshToken에 비하면 유효기간이 훨씬 짧기 때문에 1번 방법보다는 비교적 안전하다.

물론 이 방식 또한 Stateful한 요소지만, 보안 측면에서는 훨씬 개선되는 방법이다.

3) if 블랙리스트에 둘 다 저장?

보안적으로는 가장 좋은 방법이지만 이 방식은 사실상 세션 기반 인증 방식과 큰 차이가 없다. JWT의 Stateless한 특성을 무시해버리는 방법이다. 또한 리소스 측면에서도 두 배로 토큰을 저장해야 하니 부담이 생길 수 밖에 없기에 쓰지 않기로 했다.


🛡️ 세션(Session) 대신 JWT를 사용하는 이유

블랙리스트 기법을 도입하면 stateless한 특성이 사라져 JWT를 쓸 이유가 없지 않을까? 라는 생각을 할 수도 있다. 그러나 여전히 JWT의 주요 장점들은 유지되고 있다.

Stateless 관점에서

  • 세션(Session) 방식: 사용자가 로그인을 할 때, 로그인이 되어있을 때, 로그아웃을 할 때 등 모든 경우에서 항상 DB를 조회해야 한다.

  • JWT 방식:
    JWT 방식에서는 오직 로그아웃을 할 때에만 DB에 접근하면 되고 나머지 상황에서는 토큰만 발급하고 확인하면 된다.

효율성 UP

  • 세션(Session) 방식:
    세션 기반 인증에서는 서버 측에서 세션 상태를 관리해야 하기에 많은 사용자가 동시에 접속하거나, 여러 서버에서 작업하는 경우 어려움이 발생한다. 세션 상태가 여러 서버에서 일관되게 유지되어야 하므로 서버 간 데이터 공유가 필요하다.

  • JWT 방식:
    JWT는 클라이언트에게 토큰을 발급하고, 이 토큰에는 필요한 사용자 정보가 포함되어 있어서 서버의 메모리 상태나 데이터베이스에 상태를 저장할 필요가 없다. 각 토큰은 자체적으로 유효성을 가지고 있어서 별도의 서버 간 데이터 공유가 필요하지 않다.

보안

  • 세션(Session) 방식:
    만약 세션 ID를 탈취당하면 해당 사용자의 세션을 위조할 수 있는 보안 취약점이 있다. 게다가 세션 쿠키를 이용한 CSRF(사이트 간 요청 위조) 공격에도 취약하다.

  • JWT 방식:
    JWT는 토큰을 서명하여 변조를 방지하고, 필요에 따라 암호화하여 정보를 안전하게 전송할 수 있다. 무엇보다 토큰 자체에 유효 기간이 설정되어 있어 만료된 토큰은 사용할 수 없다.

이러한 이점 때문에 로그아웃을 처리하기 불편하다는 단점이 있지만 여전히 JWT 방식은 세션 기반 인증 방식보다 많이 쓰인다. 비록 블랙리스트에 Stateful한 요소가 추가되긴 했지만 여전히 DB 접근 또한 JWT 방식이 훨씬 우세하다.


마치며

JWT 방식은 초반엔 매우 복잡하게 느껴지고, 그 중에서도 로그아웃 기능은 큰 산이나 다름없다. 코드만 본다면 이해하기 어렵지만 원리를 이해하고 코드를 짜본다면 조금씩 JWT 방식을 이해할 수 있게 될 것이다. 앞으로도 열심히 공부하자!

profile
백엔드 개발자

0개의 댓글

관련 채용 정보