Intro
유저 인증은 기본적이면서도 중요한 절차다.
JWT를 사용하고 있지만 토큰이 어떻게 만들어지고, 왜 JWT가 유용한지 모르는 개발자들이 꽤 많은 것 같다.
JWT를 제대로 알고 사용해보자.
Session based authentication
JWT의 장점을 알기 위해서는 먼저 세션 인증을 알아야 한다.
JWT를 사용하기 이전에는 주로 세션을 이용한 인증이 이루어졌다.
세션 인증
세션 인증은 아래 그림과 같이 이루어진다.
세션 인증 과정에 대해서 간단히 설명하면 다음과 같다:
1.
클라이언트에서 사용자의 인증 정보를 서버에 전달한다.
2.
서버는 사용자 정보를 확인한 뒤 해당 사용자에 대해 세션을 생성합니다.
3.
세션 정보는 DB(또는 캐시)에 저장하고, 클라이언트는 session id를 받아 로컬 스토리지에 저장한다.
4.
클라이언트는 이후 이루어지는 요청에 session id를 이용한다.
5.
서버는 전달 받은 session id를 이용하여 저장 중인 세션 정보로 인증을 처리한다.
6.
만약 session id가 만료되었을 경우에는 1번 과정부터 다시 이루어진다.
여기서 중요한 점은 세션 정보를 서버에서 관리한다는 사실이다.
클라이언트도 로컬 스토리지에 session id를 저장하여 사용하지만 session id 자체는 중요한 정보가 담겨있지 않은 일종의 임시 비밀번호이며, 실제 세션 정보를 관리하는 것은 전적으로 서버의 역할이다.
Django에서 기본적으로 제공하는 세션 인증을 사용해보면 django_session 테이블이 생성되고 그 안에 session_key, session_data, expire_date와 같은 필드로 세션 정보가 저장된다.
세션 인증의 단점
세션 인증의 단점은 매 요청마다 인증을 위해 스토리지(보통 DB)를 탐색해야 한다는 점이다.
모든 요청마다 인증을 처리하기 위해 과도한 스토리지 I/O가 필요하다.
이런 배경에서 탄생한 것이 바로 JWT다.
그럼 JWT는 어떤 방식으로 동작하길래 세션인증이 갖는 문제점을 해소할 수 있을까?
JWT - Token based authentication
JWT의 가장 큰 차별점은 토큰 자체에 유저의 정보가 담겨있다는 점이다.
세션인증은 모든 유저의 정보와 세션정보를 서버에서 관리하지만 JWT 인증에서는 토큰에 유저의 정보를 담는다.
토큰에 유저의 정보가 담긴다면 "보안은 어떻게 해결할까?"하는 궁금증이 자연스럽게 떠오를 것이다.
그렇다면 아래 세 가지에 대해 자세히 알아보며 JWT가 세션보다 우수한 점이 무엇인지 생각해보자.
1) JWT를 이용한 인증 방식이 어떻게 이루어지는지
2) JWT에 유저의 정보를 어떻게 담는지
3) 나아가 보안 문제를 어떻게 해결하는지
JWT 동작 방식
JWT 인증은 아래와 같이 이루어진다.
JWT 인증 과정을 설명하면 다음과 같다:
1.
(세션인증과 마찬가지로) 클라이언트에서 사용자의 인증 정보를 서버에 전달한다.
2.
서버는 인증 정보로 인증을 처리하고 (세션 대신) JWT를 생성하여 클라이언트에 전달한다.
3.
클라이언트는 JWT를 로컬 스토리지에 저장한다.
4.
클라이언트는 이후 이루어지는 요청에 JWT를 헤더에 담아 요청한다.
5.
서버는 JWT를 검증하여 인증을 처리한다.
6.
JWT가 만료되면 토큰을 재발급(refresh)한다.
6번 JWT 재발급(refresh) 과정은 별도로 설명하지 않는다.
•
3번 과정에서 세션 정보가 서버에 저장되는 반면 JWT 인증에서는 서버에 아무것도 저장되지 않는다.
•
5번 과정에서 세션 확인을 위해 데이터 베이스를 탐색하지만, JWT 인증에서는 토큰을 바로 검증한다.
세션 인증과 JWT 인증의 가장 큰 차이점은 서버에 인증 정보를 저장하지 않는다는 점이다.
그렇기 때문에 클라이언트의 요청마다 인증을 위해 스토리지를 탐색하는 과정이 필요하지 않다.
Django에서 JWT를 사용해보면 세션 인증과 달리 별도의 테이블이 생성되지 않는다.
BlackList를 관리하는 경우에는 테이블이 생성된다
JWT는 토큰 안에 유저의 정보를 담고 있기 때문에 별도의 데이터 및 저장 공간이 필요하지 않다.
그럼 JWT는 어떻게 토큰 안에 유저의 정보를 담으면서 보안까지 갖추었을까?
JWT 구조
이제 JWT가 생성되는 원리에 대해서 알아보자.
JWT는 아래의 예시처럼 .을 이용하여 크게 세 개의 영역으로 구분되고 각각의 영역은 고유의 역할이 있다.
xxxxx.yyyyy.zzzzz
Python
복사
먼저 xxxxx 부분은 Header 영역이다. 쉽게 말해 JWT의 메타 정보를 나타낸다.
토큰의 타입을 정의하고(typ), 어떤 서명(signing) 알고리즘이 쓰였는지(alg)를 나타낸다.
JSON으로 나타내면 다음과 같다:
{
"typ": "JWT",
"alg": "HS256"
}
JSON
복사
위의 JSON 정보는 Base64Url로 인코딩 되어 JWT의 첫 번째 영역이 된다.
그 다음 yyyyy 부분은 Payload로 불리는 영역이다.
이 부분에 토큰이 만료되는 시간, 유저의 정보와 같은 실질적인 데이터를 담는 영역이다.
아래의 예시를 보면 알겠지만 JWT에 담기는 유저 정보는 인증 정보가 아닌 유저를 특정할 수 있는 값이다.
{
"token_type": "access",
"exp": 1649145719,
"jti": "1foo2jwt3id4",
"user_id": 123
}
JSON
복사
payload 역시 Base64Url로 인코딩 되어 JWT의 두 번째 영역이 된다.
JWT Signature
마지막 zzzzz 부분은 JWT의 핵심인 Signature 영역이다.
JWT는 서명을 통해 토큰 안에 유저 정보를 담으면서도 이를 안전하게 처리한다.
Payload 영역에 담긴 유저 정보는 인코딩만 되어 있지 별도의 암호화 처리가 되어 있지 않다.
누군가에 의해 쉽게 디코딩 될 수 있고 변조 될 수 있다는 말이다.
그런데 payload에는 유저를 특정할 수 있는 user id만 담았기 때문에 위의 토큰이 디코딩되어 user id가 노출되는 것은 보안상 큰 문제가 되지 않는다.
user id를 통해 유저의 숫자가 노출될 수 있기 때문에 uuid를 쓰는 것이 조금 더 나은 방법일 수도 있다.
그렇다면 이제 변조의 문제만 해결하면 된다.
토큰의 변조는 signature 영역에서 해결된다.
Signature가 만들어지기 위해서는 인코딩 된 header, 인코딩 된 payload 그리고 secret이 필요하다.
Simple JWT에서는 secret으로 Django 프로젝트마다 사용하는 secret_key를 기본으로 이용한다.
Sinature는 인코딩 된 header, payload, secret을 합친 뒤 이를 header에 지정한 알고리즘으로 해싱한다.
header와 payload, secret 값 중 어느 하나라도 일치하지 않으면 signature는 완전히 다른 값을 갖게 된다.
이렇게 생성된 JWT는 클라이언트에 전달 되었다가 이후 요청에 HTTP Header에 담겨서 서버로 전달된다.
서버가 JWT를 검증하는 과정은 JWT가 생성될 때와 마찬가지로 header, payload 그리고 secret을 이용하여 signature를 해싱한 뒤 전달받은 JWT의 signature와 같은지 확인한다.
만약 payload가 변조 되었다면 클라이언트에서 받은 signature와 서버에서 해싱한 signature가 다를 것이다.
마침내 토큰이 검증되면 서버는 payload에 담긴 유저의 정보를 통해 유저를 특정하고 인증된 유저로 처리한다.
그렇기 때문에 JWT 인증은 별도의 데이터베이스 탐색 없이도 빠르게 유저 인증을 수행할 수 있다.
마치며
세션과의 비교를 통해 JWT의 핵심을 정리했다.
주제에 집중하기 위해 JWT 재발급 같은 일부 과정에 대한 설명은 생략하였다.
실제 사용중인 JWT를 디코딩 해보면 위의 설명을 더 쉽게 이해할 수 있을 것이다.
아래의 디버거를 이용해 직접 토큰을 인코딩/디코딩 해보길 권장한다.
JWT와 비교를 위해 세션 방식의 단점을 강조하였지만 세션인증은 널리 쓰이는 인증 방식이며 강점이 존재한다.
Session을 이용하면 서버에서 유저의 ID를 적극적으로 관리할 수 있다.
예를 들어 Netflix에서 공유 중인 계정의 수를 제한하는 기능은 세션을 이용하여 구현한 것이다.
각 인증 방식의 장단점을 정확히 알고 있어야 필요에 맞는 기술을 선택할 수 있다.