인증에 사용되는 토큰의 일종인 JWT(JSON Web Token)를 알아보기 전에 토큰 외의 다른 인증방식에 대해 먼저 알아보자.
서버에서 클라이언트를 인증하는 방법은 크게 쿠키, 세션, 토큰방식으로 나눌 수 있다.
쿠키란 Key-Value형식의 문자열로, 서버의 정보를 클라이언트의 브라우저에 저장하기 위해 사용한다. 서버에서 응답에 Set-Cookie헤더를 통해 여러 정보들을 클라이언트의 브라우저에 쿠키로 등록하면 이후 매 요청마다 쿠키값이 같이 전송되기 때문에 브라우저를 식별할 수 있다.
쿠키를 이용한 인증방식은 다음과 같은 과정을 통해 이루어진다.
쿠키를 이용한 인증방식은 인증정보를 클라이언트에 저장하기 때문에 서버의 리소스를 절약할 수 있다는 장점이 있지만, 인증정보가 클라이언트에 저장되기 때문에 보안에 취약하고 유출 및 위/변조 위험이 있다. 또한 쿠키의 용량이 제한적이기 때문에(도메인당 20개, 쿠키당 4KB) 많은 정보를 저장할 수 없고, 브라우저끼리 쿠키 공유가 불가능하며 쿠키가 쌓일수록 네트워크에 부하가 심해진다는 단점이 있다.
이러한 단점 때문에 클라이언트의 인증정보를 서버에서 관리하는 세션방식의 인증이 등장하였다.
세션이란 서버에서 보관하는 인증정보로, Key(session_id)-Value(map)형식으로 데이터를 저장한다. 세션방식의 인증에서는 클라이언트와 아무 의미가 없는 session_id를 주고받기 때문에 이 값이 노출되어도 어떤 정보를 보관하고 있는지 알 수 없다는 장점이 있다.
세션을 이용한 인증방식은 다음과 같은 과정을 통해 이루어진다.
세션을 이용한 인증방식은 클라이언트에 세션 아이디값만 저장하기 때문에, 이 값이 노출되어도 사용자의 정보가 유출되지는 않는다는 장점이 있다. 하지만 세션 아이디 자체가 탈취당할 경우, 공격자는 서버에 클라이언트인 척 위장하여 요청을 보낼 수 있고, 서버에서는 매 요청마다 저장소에서 세션을 찾아야하기 때문에 요청이 많아지면 서버에 부하가 늘어난다는 단점이 있다.
세션기반 인증방식의 가장 큰 문제점은 사용자의 인증정보가 서버에 저장되어 요청시마다 세션을 조회하면서 많은 오버헤드가 발생한다는 점이었다. 토큰기반 인증방식은 이 문제를 해결하기 위해 인증정보를 다시 클라이언트에 보관한다. 토큰은 서버에서 해당 클라이언트가 인증되었다는 의미로 발급해주는 유일한 문자열로, 토큰을 받은 클라이언트는 이후 요청마다 헤더에 토큰을 추가하여 서버로 전송한다.
토큰을 이용한 인증방식은 다음과 같은 과정을 통해 이루어진다.
이때 토큰에는 자체적으로 사용자의 정보가 들어있기 때문에, 저장소를 조회하지 않고도 요청을 보낸 클라이언트를 식별할 수 있다.
세션기반 인증방식과 토큰기반 인증방식의 가장 큰 차이점은 서버에서 클라이언트의 상태를 유지하는지 여부이다. 세션방식은 클라이언트의 정보가 담긴 세션을 별도의 저장소에 보관해두어야 하기 때문에 (Stateful) 서버에 부하가 크고, 확장성이 떨어진다. 하지만 토큰방식은 요청마다 토큰을 검증하여 클라이언트를 식별할 수 있기 때문에 클라이언트의 정보를 서버에서 유지하지 않아도 되고 (Stateless), 따라서 세션방식에 비해 부하가 적다.
하지만 토큰은 쿠키/세션에 비해 데이터의 길이가 길어 요청이 많아질수록 네트워크에 부하가 걸리고, 토큰의 페이로드는 암호화되지 않아 중요한 정보를 담을 수는 없다. 또한 특정 토큰을 무력화할 수 있는 방법이 없어 토큰이 탈취당하면 대처가 어렵다는 단점이 있다.
JWT란 인증에 필요한 정보를 암화화한 토큰의 일종으로, JSON형식의 데이터를 Base64 URL-Safe Encode방식으로 인코딩한 것이다. 또한 토큰에 개인키를 통한 전자서명이 포함되어있기 때문에 위/변조에 안전하다는 장점이 있다.
JWT는 크게 Header, Payload, Signiture 세 부분으로 이루어져 있다. 각 파트는 .을 통해 구분한다.
{
"alg": "HS256",
"typ": "JWT"
}
JWT의 헤더에는 두 가지 필드가 존재한다. alg는 서명을 암호화한 알고리즘의 종류가 담겨있고, typ은 토큰의 유형(JWT)을 나타낸다.
{
"sub": 1234, // registed
"name": "impala", // private
"role": "admin", // private
"iat": 123412341234, // registed
"https://velog.io/@impala": true // public
...
}
JWT의 페이로드 부분에는 Claim이라고 부르는 인증정보의 조각들이 담겨있다. Claim은 페이로드의 key-value쌍으로, 서버에서 클라이언트를 인증하기 위해 사용되는 정보가 담겨있다.
Claim의 종류는 Registed claim, Public claim, Private claim 3가지로 각각 다음과 같은 의미를 가진다.
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload) + "." +
256-bit-secret
)
JWT의 서명은 헤더에서 선택한 암호화 알고리즘을 사용하여 헤더와 페이로드를 각각 인코딩한 문자열과 서버의 비밀키값을 .으로 연결하여 암화화한다. 이때 헤더와 페이로드는 단순하게 base64로 인코딩된 값이므로 누구나 확인할 수 있지만, 서명은 서버의 비밀키값을 사용하기 때문에 비밀키가 유출되지 않는 이상 위/변조가 불가능하다.
JWT를 사용한 인증과정은 다음과 같다.
검증 과정에서 서버는 클라이언트로부터 받은 토큰을 파싱하여 헤더와 페이로드를 추출하고, 해당 데이터를 비밀키를 통해 다시 서명을 만들어서 두 토큰을 대조해본다. 이때 전자서명에는 단방향 해시함수를 사용하기 때문에, 클라이언트로부터 받은 토큰이 위/변조된 것이라면 서명값이 달라 위/변조 사실을 인지할 수 있다.
JWT는 일반적인 토큰과 마찬가지로 제 3자에게 토큰을 탈취당하면 이 사실을 알아차리기 어렵기 때문에, Refresh Token을 사용하여 2중으로 인증을 수행한다. Refresh Token 역시 일반적인 JWT이지만 Access Token보다 수명이 길기 때문에 Access Token이 만료되어도 Refresh Token을 사용해 클라이언트를 인증하고 새로운 Access Token을 발급해 줄 수 있다.