JWT로 인증 처리하기

손문기·2023년 12월 10일
0
post-thumbnail

시작하며

프로젝트를 진행하며 사용자 인증을 구현했던 과정에 대해 적어보고자 합니다.

우선 저는 JWT를 accessToken과 refreshToken으로 나누어 사용하였으며, RTR(Refresh Token Rotation) 방식을 도입하고 refreshToken을 Redis에서 관리하여 토큰이 탈취당하는 경우에 대비 할 수 있도록 구현하였습니다.

이러한 구현을 하면서 했던 고민과 선택, 그리고 그 이유에 대해 적어보도록 하겠습니다.

왜 JWT를 사용했나?

인증을 구현하면서 쿠키/세션 방식이 아닌 JWT를 사용한 이유는 다음과 같습니다.

  • 서버가 사용자의 상태를 저장하여 관리하지 않는다.
  • 플랫폼(웹, 모바일 등) 호환성이 높다.

쿠키/세션 방식은 서버에서 사용자의 정보를 저장하여 상태를 관리해야하는 반면, JWT는 토큰에 대한 유효성 검사만을 하면 되므로 서버의 확장성과 성능적인 측면에서 유리합니다.

현재 프로젝트은 안드로이드 어플리케이션을 개발하는 프로젝트로 주로 웹 브라우저 환경에서 사용되고 관리는 쿠키/세션 방식 보다는 플랫폼간의 호환성이 높은 JWT가 적절하다고 판단하였습니다.

JWT는 장점만 있나?

위에서 장점으로 언급되었던 무상태성이 보안적인 측면에서는 단점이 됩니다.

서버가 상태를 관리하지 않는 다는 것은 토큰이 탈취 당하더라도, 서버는 해당 토큰이 정상적인 토큰인지 탈취된 토큰인지 알 수 없다는 의미와 같습니다. 따라서 서버는 해당 토큰이 만료되기 전까지 어떠한 제어도 할 수 없습니다.

accessToken과 refreshToken을 사용하는 이유

이를 보완하기 위해 토큰에 대한 유효기간을 짧게 설정하여 토큰이 탈취당한 경우에 대한 영향을 최소하 할 수 있습니다. 하지만 토큰의 유효기간이 짧을 경우 토큰을 재발급 받기 위해 사용자가 자주 로그인을 해야하여 사용자 경험이 저하될 수 있습니다.

이를 해결하기 위해 만료된 토큰을 재발급 하기 위한 유효 기간이 비교적 긴 refreshToken을 함께 사용합니다.

그렇다면 refreshToken이 탈취당하면 어떻게 하나?

refreshToken의 경우 accessToken을 갱신하는 요청에만 사용되기 때문에, 비교적 탈취에 노출되기 쉽지는 않습니다. 하지만 refreshToken의 유효기간은 매우 길기 때문에 한 번이라도 탈취된다면 그 영향이 치명적일 수 있습니다.

RTR(Refresh Token Rotation)을 도입하자

RTR(Refresh Token Rotation) 방식은 accessToken을 새로 발급하면 refreshToken 또한 새로 발급하는 방식이다. 이를 통해 앞으로 refreshToken은 accessToken을 재발급 하기 위한 1회용 토큰으로 사용되어 토큰이 탈취 당하여 악용되는 것을 방지 할 수 있습니다.

refreshToken을 서버에서 관리하자

서버에서 refreshToken을 저장하여 관리하는 것을 통해, 앞서 도입한 RTR로 이미 사용된 refreshToken에 대한 접근을 무효화 할 수 있습니다.

어디에 refreshToken을 저장할건가?

저는 refreshToken은 Redis에 저장하여 사용하고 있습니다.

그 이유로는 redis의 경우 인메모리 DB로 속도가 다른 DB에 비해 빠르며, 만약 데이터가 유실 되더라도 사용자가 재 로그인만 하면 되므로 큰 문제가 발생하지 않습니다.

이와 같은 장점으로는 서버 로컬 메모리를 사용하는 방안도 있을 수 있으나, 이는 저장 공간의 한계와 서버 확장성의 문제가 있습니다.

어떻게 이전 refreshToken 요청을 무효화 하는 방법

이를 구현하기 위한 방법으로 저는 2가지를 떠올렸습니다.

  1. 사용자별로 최신 refreshToken만을 저장하여, 요청을 허용할 whiteList를 관리하자
    • 장점: 데이터를 사용자별 1개만 유지할 수 있어 저장 공간이 적다.
    • 단점: 갱신 요청마다 항상 값을 찾아서 변경해야 한다.
  2. 이미 재사용된 refreshToken을 저장하여, 요청을 거부할 blackList를 관리하자
    • 장점: 갱신 요청시 값을 추가만 하면 된다.
    • 단점: 데이터가 누적된다.

저는 1번을 선택하여 구현하였는데 이유는 다음과 같습니다.

  • refreshToken을 저장하는 저장소를 redis를 사용하고 있어, 토큰을 갱신하는 과정에서 1번과 2번의 성능적 차이가 미미하다.
  • 2번의 경우, 사용자가 많아지거나 악의적인 갱신 요청으로 요청이 엄청나게 많이 들어올 경우 저장공간이 부족할 수있다.
  • 시간이 지날 수록 저장해야 할 데이터가 늘어난다. (이는 redis의 ttl을 통해 어느정도 보완이 가능하다)

따라서 저는 1번의 방식을 선택하여 사용자별 최신 refreshToken을 서버에 저장하여 관리하고 있고, accessToken을 갱신하기 위한 요청으로 들어온 refreshToken을 서버에 저장되어 있는 데이터와 비교하여 유효성 검사를 진행하고 있습니다.

최신 refreshToken을 탈취당하면?

위와 같은 보안적 과정에도 불구하고, 최신 refreshToken을 탈취당할 경우에 대한 허점은 존재합니다.

해커가 최신 refreshToken을 탈취하는 경우 해당 토큰을 시작으로 지속적으로 accessToken 갱신 요청을 할 수 있으며, 오히려 기존의 정상 사용자의 요청은 거절당할 것입니다.

이를 방지하기 위해서는, 이미 사용된 refreshToken으로 요청이 들어오면 기존의 최신 refreshToken까지 모두 무효화 하는 과정이 필요합니다. 이를 통해 해커가 지속적으로 갱신하는 것을 방지 할 수 있으며, 사용자는 재 로그인을 통해 토큰을 재발급 받아 정상적인 이용을 이어나갈 수 있습니다.

이렇게 서버에서 해주는 일이 많으면 JWT를 쓰는 이유가 있나?

이처럼 refreshToken을 서버에 저장하여 확인하는 과정을 가질거면, 차라리 쿠키/세션 방식을 사용하는게 낫지 않나? 라는 의문을 가질 수 있습니다.

위와 같은 방식들이 JWT가 가진 무상태성의 이점을 저해하는 것은 맞습니다. 하지만 여전히 JWT가 가지는 이점이 크다고 생각합니다.

쿠키/세션 방식은 매 요청마다 I/O가 발생하지만, JWT는 accessToken을 갱신하는 요청에 대해서만 I/O가 발생하므로 그 비용이 현저히 적다고 할 수 있습니다.

마치며

사용자 인증은 정말 많은 다양한 방법으로 구현할 수 있기에, 정해져있는 정답은 없다고 생각합니다.
제가 선택한 JWT 또한 단점을 보완하기 위해 성능, 보안, 편이성간의 trade off를 고려하여 타협하였듯이, 각자의 주어진 상황에 맞는 선택을 고민 할 필요가 있습니다.
오늘 적어본 사용자 인증 뿐만 아니라 앞으로 있을 수 많은 개발에 있어, 자신의 상황에 맞는 trade off를 찾아가는 과정이 가장 중요한 것은 아닐까? 생각하며 글을 마칩니다.

0개의 댓글