JWT는 Authentication(인증)보다는 Authorization(인가)와 연관된 기술이다.
로그인을 구현함에 있어서 로그인된 상태가 유지되도록 하는 것은 매우 중요하다. 어떤 사이트나 서비스에 사용자가 로그인해있다는 사실을 서버가 인지할 수 있도록 하는 방법을 어떤 것이 있을까?
Session
우선 기존에 많이 사용하던 세션 방식을 생각해보자.
사용자가 로그인에 성공하면, 서버는 Session ID라는 걸 발급한다.
쉽게 비유를 해보자면 Session ID를 영화티켓이라 보고, 서버는 영화티켓을 발급한뒤 반쪽은 사용자의 브라우저(클라이언트)로 보내고 반쪽은 자기 책상, 즉 메모리에 올려두는 것이다. (경우에 따라서는 서랍인 하드디스크나, 창고인 데이터베이스에 올려두기도 한다.)
이때 표 반쪽을 받은 사용자의 브라우저는 Session ID라는 쿠키로 저장해두고, 해당 사이트에 요청을 보낼때마다 이 Session ID를 헤더에 실어보낸다.
요청에 이 세션 ID가 실려오면 서버는 자기 책상 위, 메모리에 맞는 짝이 있는지 찾아보고 있다면 Authorization을 해주게 된다.
이렇게 Session ID를 사용해 어떤 사용자가 서버에 로그인 되어있음이 지속되는 상태를 세션이라 한다.
Session의 문제
세션은 메모리에다 티켓 반쪽을 올려두기 때문에 동시접속자가 많아지면 메모리가 부족해진다는 허점이 있다.
또한 메모리는 휘발성이기 때문에 서버에 문제가 있어 꺼져버리거나 한다면 메모리에 있던 것들어 모두 날아간다는 문제가 있다.
이런 경우 사용자들은 로그인이 모두 튕겨서 재로그인을 해야 하는 상황이 발생하게 되는 것이다..!
세션의 더 큰 문제는 서버를 여러 대를 두고 사이트를 운영할 때이다.
서버가 여러대 있다는 것은 서버 여러개가 각자 책상과 서랍을 두고 일을 하고 있고, 사용자의 요청이 들어올 때 마다 여러 서버들이 요청을 분산하여 처리하는 것인데,
이때 로그인은 1번 서버가 처리하고 이후 이메일 페이지로 가는 요청은 3번 서버가 처리하면 3번 서버에 책상이나 서랍에는 사용자의 티켓 반쪽이 없기 때문에 세션 유지가 제대로 되지 않는 것이다.
이를 해결하기 위해서 사용자의 티켓을 공용 창고 즉, 데이터 베이스 서버에 넣어둘 수도 있지만 이렇게 하면 속도가 많이 느리다.
더 흔하게는, 레디스나 MemCached 같은 메모리형 데이터베이스 서버 즉, 길다란 공용 책상을 따로 둬서 그 위에 올려두기도 하지만 이러한 방법도 메모리가 엎어졌을 때 위험이 있다.
이렇게 서버가 복잡한 구성과 환경에서 어떠한 상태를 기억하고 유지하는 것이 굉장히 설계하기 어렵다.
따라서 그런 부담 없이 인가를 구현하기 위해 나온 것이 토큰 방식인 JWT이다.
JWT
JWT를 사용하는 서비스에서는, 사용자가 로그인을 하면 서버는 Session ID와 비슷하게 토큰이라는 티켓를 발급해준다.
하지만 반쪽을 찢어서 주는 세션 방식과 달리 그냥 준다.
이는 서버가 뭔가를 기억하고 있지 않음을 의미한다. 토큰만 주고 서버는 아무것도 기억하지 않는다는 것이다.
발급해주는 토큰은 인코딩 또는 암호화된 3가지 데이터를 이어붙힌 형태를 가진다.
토큰은 마침표(.)로 끊어져서 3부분으로 나뉘는데 각각 header, payload, verify signature로 구분된다.
payload
우선 payload부터 알아보자.
payload 부분을 Base64로 디코딩해보면 JSON형식으로 여러 정보가 담겨있다.
이 정보에는 해당 토큰을 누가 누구에게 발급했는지, 언제까지 유효한지, 서비스가 사용자에게 이 토큰을 통해 공개하길 원하는 내용(사용자 닉네임, 서비스상의 레벨, 관리자 여부 등)을 서비스 측에서 담을 수 있다.
이렇게 토큰에 담긴 사용자 정보 등의 데이터를 Claim이라 한다.
정리해보자면, 사용자가 로그인을 하고 나서 받는 토큰에 여러 정보들이 Claim이라는걸로 실려오게 되고, 사용자는 그 이후의 요청들마다 토큰을 함께 보낸다.
서버 입장에서는 요청과 함께 사용자로부터 전달받은 토큰 자체에 여러 정보들이 들어있기 때문에 서버가 요청마다 일일이 찾아야 하는 것들이 줄어들게 되는 것이다.
그런데, 특별한 암호화도 아니고 Base64로 인코딩된 것이면 사용자가 다시 디코딩해서 볼 수 있는 것이고 그러면 사용자가 이를 조작해서 악용할 수 있는 게 아닌가?🙄하는 의문이 들 수 있다.
이러한 이유 때문에 header, verity Signature부분이 존재한다.
header & verity Signature
header 부분을 디코딩해보면 type과 alg 두가지 정보가 담겨있다.
이때 토큰의 타입은 항상 JWT로 고정값이다.
alg는 알고리즘의 약자인데, 여기에는 verity Signature를 만드는데 사용될 알고리즘이 지정된다.
예를 들어 HS256 등 여러 암호화 방식 중 하나를 지정할 수 있다.
header와 payload, 그리고 "서버에 감춰놓은 비밀 값"이 3가지를 이 암호화 알고리즘에 넣고 돌리면 verity Signature 값이 나오게 된다.
이 암호화 알고리즘이라는 것은 한쪽 방향으로는 계산이 되어도 반대쪽으로는 안되는 것이기 때문에, 서버만 알고 있는 비밀 값을 찾아낼 방법이 없으며,
따라서 토큰을 탈취해도 글자 하나만 달라지면 verity Signature값이 완전히 달라지기 때문에 payload 부분을 디코딩하여 조작한다 해도 유효한 verity Signature값이 나오려면 서버에 숨긴 비밀 키를 알고 있어야 하는 것이기 때문에 조작이 불가한 것이다.
정리
정리해보자면, 서버는 요청에 토큰 값이 담겨오면 header와 payload의 값을 "서버의 비밀 키"와 함께 돌려봐서 계산된 결과 값이 verity Signature값과 일치하는 하는지 확인하는 것이다.
이때 일치하지 않는다면 정보를 조작한 사용자이거나 해커인 것으로 간주되어 인가를 거부한다.
JWT를 사용하면 서버는 사용자들의 상태를 따로 기억해둘 필요 없이 비밀 값만 가지고 있으면 요청이 들어올 때마다 토큰을 스캔해가지고 사용자들을 걸러낼 수 있다.
JWT가 무조건 짱짱맨?
그럼 JWT가 무조건 좋고 JWT만 사용하면 될까?
세션을 대체하기에는 JWT에는 큰 결점이 있다.
JWT의 결점
세션처럼 stateful해서, 모든 사용자들의 상태를 기억하고 있는 건 구현하기도 부담되고 고려사항도 많지만, 이것이 되기만 한다면 기억하는 대상의 상태를 언제든지 제어할 수 있다는 의미이다.
예를 들어 1개의 기기에서만 로그인이 가능한 서비스를 만드는 경우 PC에서 로그인 한 사용자가 핸드폰으로 또 로그인을 할 경우 PC에서는 로그아웃되도록 기존 세션을 종료할 수 있는 것이다.
세션 방식에서는 책상에 올려놓은 기존 티켓을 버리기만하면 되는데, JWT에서는 이미 줘버린 토큰을 뺏을 수도 없으며 토큰의 발급 내역이나 정보를 서버가 어디 기록해서 추적하고 있는 것이 아니므로 통제가 불가능하다.
더 심각한 경우로, 어떤 토큰이 탈취당한 경우 해당 토큰을 무효화할 방법 또한 없다.
이런 점 때문에 실제 서비스중에 JWT만으로 인가를 구현하는 곳은 그리 많지 않다.
해결 방법
이런 점을 나름대로 보완해서 사용하기 위해서는 만료시간을 아주 짧게, 즉 토큰의 수명을 아주 짧게 주는 방법이 있다.
하지만 이런경우 얼마 지나지 않아 또 로그인을 해야되지 않나? 생각해볼 수 있다.
그런 점도 고려하여 로그인을 하면 토큰 두개를 주는 것이다.
- 수명이 몇 시간이나 몇 분 이하로 짧은 access 토큰
- 꽤 길게, 보통 2주 정도로 잡혀 있는 refresch 토큰
access 토큰과 refresch 토큰을 구현하는 방법 중 하나를 간단하게 알아보자면,
access 토큰과 refresh 토큰을 발급해 클라이언트에게 보내고 나서 refresch 토큰의 상응값을 데이터베이스에도 저장한다.
사용자는 access 토큰의 수명이 다할 경우 refresch 토큰을 보내고 서버는 데이터베이스에 저장된 값과 비교해 맞다면 새 access 토큰을 발급해주는 것이다.
그렇다면 이제 refresh 토큰만 안전하게 관리된다면 이것이 유효할 동안에는 access 토큰이 만료될 때마다 다시 로그인 할 필요 없이 새로 발급 받을 수 있게된다.
access 토큰을 재발급 받을 때 쓰는 것이 refresh 토큰인 것이며, 중간에 access 토큰을 탈취당해도 수명이 짧기 때문에 오래 쓰지 못하게 된다.
또한 어떠한 사용자를 강제 로그아웃 시키려면 refresh 토큰을 데이터베이스에서 삭제해 토큰 갱신이 안되도록 할 수 있다.
하지만, 그 마저도 짧은 access 토큰이 살아 있는 동안에는 이것을 바로 차단할 방법은 없기 때문에 빈틈 없는 해결책이라고 할 수는 없다.
이러한 점이 현재 JWT의 큰 한계이다.
때문에 JWT가 구현하기 편리하고 좋더라도 인가 방식에 JWT를 적용하기에 나의 서비스가 적합한지 충분한 고려가 필요하다 !
Reference
'🕹️ Programming > 개발상식' 카테고리의 다른 글
[디자인패턴] MVC 패턴 (0) | 2023.11.03 |
---|---|
MVC를 지키면서 코딩하는 방법 (0) | 2022.11.15 |
Cookie & Session 그리고 Cache? (0) | 2022.08.23 |
Authentication & Authorization (0) | 2022.04.04 |