JWT는 강제로 만료할 수 없는데 우리는 어떻게 블랙리스트를 관리하는가
문제 개요
로그인한 사용자의 인증과 인가를 위해 jwt를 사용하기로 했다.
사용 방식은 다음과 같다.
- 로그인 시 jwt를 발급함
- 인증이 필요한 경로에는 jwt를 첨부해 요청을 보냄
- 인증이 필요한 경로에서는 요청에 포함된 jwt를 검증해 사용자를 인증함
이때 jwt를 어떻게 발급하고, 또 발급한 jwt는 어떻게 관리할 지에 대한 고민이 필요했다.
오류 메시지 및 원인 분석
사용자 인증을 위해 단순히 jwt 토큰을 발급해주는 것 하나만으로는 부족하다.
먼저, 토큰이 탈취 당할 위험을 고려해 토큰의 유효 기간은 짧게 잡는 것이 좋다. 클라이언트에 저장해놓고 request에 첨부하는 토큰의 특성상 탈취 가능성은 언제나 존재하기 때문이다.
이런 위험을 줄이고 보안을 높이는 여러 방법이 있지만 가장 간단한 것은 유효 기간을 가능한 한 줄이는 것이다. 이 방법은 토큰이 탈취 당하더라도 해당 토큰을 사용할 수 있는 시간이 짧기 때문에 피해 범위를 최소화할 수 있다고 간주한다.
그렇다고 유효 기간을 줄여 놓기만 하면 사용자 경험이 매우 악화될 수 있다.
위와 같이, 혹은 이보다 로그인 유지 시간을 더 짧게 가져가고 매번 다시 로그인 하도록 할 수도 있지만 과한 보안이 언제나 좋은 것은 아니다.
단적으로 말해 1분마다 다시 로그인 해야 하는 서비스를 이용할 사용자는 그렇게 많지 않을 것이다.
해결 및 고민 과정
그래서 일반적으로 jwt로 사용자 인증을 할 때는 유효 기간이 짧은 access token과 비교적 긴 refresh token을 발급해 인증 과정을 구성한다.
요청을 보낼 때는 access token으로 사용자 인증을 하고, access token이 만료되면 refresh token으로 로그인 과정 없이 새 토큰을 발급 받는다. 두 토큰이 모두 만료되면 다시 로그인하도록 안내한다.
문제는 유효 기간이 지나기 전에 무효화 시켜야 하는 토큰이다. 예를 들어 아래와 같은 상황을 가정해볼 수 있다.
<aside>
- 사용자가 로그인 → JWT 발급됨 (1시간 유효)
- 10분 후 로그아웃 → 클라이언트는 토큰 폐기
- 하지만 공격자가 그 JWT를 탈취하고 저장해둠
- 남은 50분 동안은 그 JWT를 이용해 아무 제한 없이 접근 가능 </aside>
이런 문제는 jwt가 기본적으로 Stateless하기 때문에 발생한다.
- 세션 인증과 Stateless세션 방식은 로그인한 사용자를 모두 서버에서 알고 관리해야 하기 때문에 이것만으로도 리소스를 크게 사용해야 한다. 또한 서버를 수평 확장하게 되면 모든 서버에서 세션을 공유해야 하므로 확장에 매우 불리하다.항목 JWT (Stateless) 세션 (Stateful)
인증 상태 클라이언트가 유지 서버가 유지 서버 확장 쉬움 세션 공유 필요 로그아웃 처리 블랙리스트 필요 세션 삭제하면 끝 토큰 위조 서명으로 방지 서버 검증 필요 없음 서버 저장소 필요 ❌ 없음 ✅ 필요함 (메모리/Redis 등) - stateless한 인증 방식이 나오기 전까지는 세션 기반의 인증 방식을 사용했다.
결론적으로 jwt는 빠르고 확장성 높은 인증 시스템을 구성 가능하게 하지만 동시에 stateless이기 때문에 토큰 유효성에 관한 문제는 직접 설계가 필요하다.
이를 위해 블랙리스트 관리 방식, jwt의 대안 등을 고민했고, 최종적으로는 access token + refresh token + black list로 토큰을 발급/관리하는 구조를 설계했다.
- jwt의 대안Spring Security + JWT
- 질문: JWT 블랙리스트 관리 전략에 대해 좀 더 자세히 알고 싶습니다. 현재 Access Token을 redis에 블랙리스트에 등록하고 있는데, 실무에서는 어떤 방식으로 만료 된 토큰을 정리하고 관리하시나요?
최종 해결책 및 구현
jwt 블랙리스트 관리를 위해 두 가지 정책을 사용했다.
- refresh token을 발급하면 redis에 저장하고, 로그아웃 시 삭제한다.
- refresh token은 발급과 동시에 redis에 저장되고 유효 기간이 지나거나 로그아웃 하면 삭제된다.
- 즉, redis에는 유효한 refresh token이 저장된다.
- 로그아웃하면 access token을 redis에 저장한다.
- access token은 로그아웃과 동시에 redis에 저장되고 유효 기간이 지나면 삭제된다.
- 즉, redis에는 유효하지 않은 access token이 저장된다.
(*redis가 도커(인메모리)로 되어 있어서 재배포 시 리셋되는 문제가 있다.)
추가 개선점
블랙리스트 정책을 개선하여 redis에 저장하는 token을 줄일 수 있을 것 같다.
jwt 대신 paseto 사용을 고려할 수 있을 것 같다(Spring Security + JWT).