프로젝트를 진행하며 사용자 인증을 구현했던 과정에 대해 적어보고자 합니다.
우선 저는 JWT를 accessToken과 refreshToken으로 나누어 사용하였으며, RTR(Refresh Token Rotation) 방식을 도입하고 refreshToken을 Redis에서 관리하여 토큰이 탈취당하는 경우에 대비 할 수 있도록 구현하였습니다.
이러한 구현을 하면서 했던 고민과 선택, 그리고 그 이유에 대해 적어보도록 하겠습니다.
인증을 구현하면서 쿠키/세션 방식이 아닌 JWT를 사용한 이유는 다음과 같습니다.
쿠키/세션 방식은 서버에서 사용자의 정보를 저장하여 상태를 관리해야하는 반면, JWT는 토큰에 대한 유효성 검사만을 하면 되므로 서버의 확장성과 성능적인 측면에서 유리합니다.
현재 프로젝트은 안드로이드 어플리케이션을 개발하는 프로젝트로 주로 웹 브라우저 환경에서 사용되고 관리는 쿠키/세션 방식 보다는 플랫폼간의 호환성이 높은 JWT가 적절하다고 판단하였습니다.
위에서 장점으로 언급되었던 무상태성이 보안적인 측면에서는 단점이 됩니다.
서버가 상태를 관리하지 않는 다는 것은 토큰이 탈취 당하더라도, 서버는 해당 토큰이 정상적인 토큰인지 탈취된 토큰인지 알 수 없다는 의미와 같습니다. 따라서 서버는 해당 토큰이 만료되기 전까지 어떠한 제어도 할 수 없습니다.
이를 보완하기 위해 토큰에 대한 유효기간을 짧게 설정하여 토큰이 탈취당한 경우에 대한 영향을 최소하 할 수 있습니다. 하지만 토큰의 유효기간이 짧을 경우 토큰을 재발급 받기 위해 사용자가 자주 로그인을 해야하여 사용자 경험이 저하될 수 있습니다.
이를 해결하기 위해 만료된 토큰을 재발급 하기 위한 유효 기간이 비교적 긴 refreshToken을 함께 사용합니다.
refreshToken의 경우 accessToken을 갱신하는 요청에만 사용되기 때문에, 비교적 탈취에 노출되기 쉽지는 않습니다. 하지만 refreshToken의 유효기간은 매우 길기 때문에 한 번이라도 탈취된다면 그 영향이 치명적일 수 있습니다.
RTR(Refresh Token Rotation) 방식은 accessToken을 새로 발급하면 refreshToken 또한 새로 발급하는 방식이다. 이를 통해 앞으로 refreshToken은 accessToken을 재발급 하기 위한 1회용 토큰으로 사용되어 토큰이 탈취 당하여 악용되는 것을 방지 할 수 있습니다.
서버에서 refreshToken을 저장하여 관리하는 것을 통해, 앞서 도입한 RTR로 이미 사용된 refreshToken에 대한 접근을 무효화 할 수 있습니다.
저는 refreshToken은 Redis에 저장하여 사용하고 있습니다.
그 이유로는 redis의 경우 인메모리 DB로 속도가 다른 DB에 비해 빠르며, 만약 데이터가 유실 되더라도 사용자가 재 로그인만 하면 되므로 큰 문제가 발생하지 않습니다.
이와 같은 장점으로는 서버 로컬 메모리를 사용하는 방안도 있을 수 있으나, 이는 저장 공간의 한계와 서버 확장성의 문제가 있습니다.
이를 구현하기 위한 방법으로 저는 2가지를 떠올렸습니다.
저는 1번을 선택하여 구현하였는데 이유는 다음과 같습니다.
따라서 저는 1번의 방식을 선택하여 사용자별 최신 refreshToken을 서버에 저장하여 관리하고 있고, accessToken을 갱신하기 위한 요청으로 들어온 refreshToken을 서버에 저장되어 있는 데이터와 비교하여 유효성 검사를 진행하고 있습니다.
위와 같은 보안적 과정에도 불구하고, 최신 refreshToken을 탈취당할 경우에 대한 허점은 존재합니다.
해커가 최신 refreshToken을 탈취하는 경우 해당 토큰을 시작으로 지속적으로 accessToken 갱신 요청을 할 수 있으며, 오히려 기존의 정상 사용자의 요청은 거절당할 것입니다.
이를 방지하기 위해서는, 이미 사용된 refreshToken으로 요청이 들어오면 기존의 최신 refreshToken까지 모두 무효화 하는 과정이 필요합니다. 이를 통해 해커가 지속적으로 갱신하는 것을 방지 할 수 있으며, 사용자는 재 로그인을 통해 토큰을 재발급 받아 정상적인 이용을 이어나갈 수 있습니다.
이처럼 refreshToken을 서버에 저장하여 확인하는 과정을 가질거면, 차라리 쿠키/세션 방식을 사용하는게 낫지 않나? 라는 의문을 가질 수 있습니다.
위와 같은 방식들이 JWT가 가진 무상태성의 이점을 저해하는 것은 맞습니다. 하지만 여전히 JWT가 가지는 이점이 크다고 생각합니다.
쿠키/세션 방식은 매 요청마다 I/O가 발생하지만, JWT는 accessToken을 갱신하는 요청에 대해서만 I/O가 발생하므로 그 비용이 현저히 적다고 할 수 있습니다.
사용자 인증은 정말 많은 다양한 방법으로 구현할 수 있기에, 정해져있는 정답은 없다고 생각합니다.
제가 선택한 JWT 또한 단점을 보완하기 위해 성능, 보안, 편이성간의 trade off를 고려하여 타협하였듯이, 각자의 주어진 상황에 맞는 선택을 고민 할 필요가 있습니다.
오늘 적어본 사용자 인증 뿐만 아니라 앞으로 있을 수 많은 개발에 있어, 자신의 상황에 맞는 trade off를 찾아가는 과정이 가장 중요한 것은 아닐까? 생각하며 글을 마칩니다.