본문 바로가기
Web

JWT(JSON Web Token) 란?

by jewook3617 2021. 1. 13.

웹에서 인증을 위해 많이 사용하는 JWT(JSON Web Token)에 대해 정리해보겠습니다.

우선 웹에서 인증 메커니즘이 왜 필요한지 살펴보겠습니다. 

Stateless한 HTTP

HTTP는 stateless 하다는 특징을 가지고 있습니다. 말 그대로 상태를 보관하지 않는다는 뜻입니다.

우리가 웹사이트에 접속해서 로그인을 한 후 새로고침을 했다고 가정해보겠습니다. 이 때 로그인 할 때와 새로고침 할 때, 이렇게 총 두 개의 request가 서버로 전송이 됩니다. 하지만 서버에서는 이 두 request가 같은 사용자로부터 왔다는 그 어떠한 정보도 별도로 저장해놓지 않습니다. 즉, 같은 사용자가 보낸 request라고 하더라도 서버 입장에서는 독립적인 별개의 request로 인식하는 것입니다. 이러한 특징을 HTTP의 stateless라고 부릅니다.

그렇기 때문에 별도의 인증 메커니즘이 없다면 우리가 로그인을 했더라도 다른 페이지로 이동하면 로그인 정보가 사라지게 됩니다.

그럼 JWT 등장 이전에는 어떤 방식으로 웹에서 인증이 구현되는지 살펴보겠습니다.

Cookie와 Session

JWT 등장 이전에는 cookie와 session을 이용해 인증이 구현되었습니다. 이 글은 JWT에 관한 글이므로 cookie와 session에 대해서는 간단히 정리하겠습니다.

cookie란 브라우저에 저장되는 "name=jewook" 과 같은 key=value 형태의 text 입니다. 로그인 정보나 장바구니와 같이 사용자가 페이지를 이동하더라도 계속 남아있어야하는 정보들이 cookie로 브라우저에 저장됩니다. 

크롬 개발자도구 > Application 탭 > Cookies 에서 현재 접속한 사이트에서 사용되는 쿠키 정보를 볼 수 있습니다.

즉, cookie는 클라이언트(브라우저)에 저장되는 정보입니다. 그럼 어떤 정보가 cookie로 저장되는 것일까요? 바로 session-id 입니다. session-id는 서버에 저장되는 사용자의 임시 id 입니다. 다음은 cookie와 session을 이용한 인증과정을 그림으로 나타낸 것입니다.

cookie와 session을 이용한 인증과정

클라이언트에서 처음에 서버로 로그인 request를 보내면 서버에서는 id와 pw가 맞는지 확인 후 session 저장소에 session-id라고 불리는 사용자의 임시 id를 만듭니다. 그리고 response로 그 session-id를 보내줍니다. 클라이언트는 그 session-id를 브라우저의 쿠키로 저장해놓고 다음 request에 해당 session-id를 같이 보냅니다. 그러면 서버에서는 session 저장소에서 request에 들어있는 session-id가 어떤 사용자의 session-id인지 찾습니다. session-id의 주인을 찾고 나면 해당 request가 "jewook"이라는 사용자로부터 온 request라는 것을 알 수 있습니다. 사용자가 로그아웃을 하게되면 session-id는 삭제되고 다음 로그인 시 또 다른 session-id가 생성됩니다.

cookie에 "id=jewook" 처럼 유저 정보를 그대로 저장하지 않고 session-id를 추가로 발급 받는 이유는 cookie가 보안에 취약하기 때문입니다. cookie는 브라우저에 저장되고 브라우저에서 수정이 가능합니다.

"id=jewook" 으로 저장된 cookie를 임의로 "id=admin" 으로 변경했다고 가정해봅시다. 만약 해당 웹사이트 admin 계정의 id가 admin이라면 간단한 cookie 조작을 통해 admin 계정으로 로그인 할 수 있는 것입니다. 그래서 cookie에 유저 정보를 그대로 저장하지 않고 계정별로 할당된 임시의 session-id를 저장하게 됩니다. session-id는 랜덤으로 생성된 문자열이기 때문에 admin 계정의 session-id를 알아내기는 쉽지 않습니다.

하지만 cookie와 session을 이용한 인증방식은 서버측에 별도의 session 저장소가 필요하기 때문에 사용자가 많아질수록 서버에 부담을 줄 수 있습니다. JWT를 이용한 토큰기반 인증방식은 이러한 문제를 해결할 수 있습니다.

이제 JWT에 대해 알아보겠습니다.

JWT(JSON Web Token)

JWT를 이용한 인증방식도 cookie와 session을 이용한 인증방식과 동일합니다. 클라이언트에서 로그인 request를 보내면 서버에서 JWT를 만들어 response로 보내줍니다. 그러면 클라이언트(브라우저)는 그 JWT를 cookie에 저장해놓고(cookie가 아닌 localStorage, sessionStorage에 저장할 수도 있습니다.) 다음 request에 해당 JWT를 같이 보냅니다. 그러면 서버에서 해당 request에 들어있는 JWT에 들어있는 유저 정보를 보고 request가 어떤 사용자로부터 온 request인지 알 수 있습니다.

JWT가 session-id와 다른 점은 token 자체에 모든 정보가 담겨져있으면서 위, 변조가 불가능서버측에 별도의 저장소가 필요하지 않다는 점입니다. 그럼 JWT가 어떻게 구성되어 있는지 살펴보겠습니다.

xxxxx.yyyyy.zzzzz

JWT는 위와 같이 .으로 구분되어 세 부분으로 나뉩니다. 세 부분은 각각 아래와 같은 역할을 합니다.

  • xxxxx : header

  • yyyyy : payload

  • zzzzz : signature

JWT의 구성요소를 알았으니 여기서 만든 실제 JWT를 보며 각 구성요소에 대해 더 알아보겠습니다. 한 줄로 쓰면 너무 길어져 .에서 줄바꿈을 했습니다.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9       // header
.eyJuYW1lIjoiamV3b29rIiwiaWF0IjoxNjA5Mzg5Mzg3LCJleHAiOjE2MDk1NjIxODd9     // payload
.hNZyY46gm0mXOgz7gNgDfzDDuTqAA0w5gRbqo6jqx3o     // signature

첫 번째 부분은 header이고 두 번째 부분은 payload라고 했는데 위 토큰을 보면 어떠한 정보도 담고있지 않는 것 처럼 보입니다. 그 이유는 header와 payload가 base64Url 인코딩 되어있기 때문입니다. 그럼 base64Url 인코딩은 무엇일까요?

base64Url 인코딩에 대해 알아보기 전에 base64 인코딩에 대해 알아보겠습니다. 

base64 인코딩은 이진수를 text로 변환해주는 기법입니다. 아래 그림을 통해 Boy라는 string을 base64 인코딩 하는 과정을 살펴보겠습니다.

Boy를 base64 인코딩 하는 과정

base64 인코딩 과정은 굉장히 간단합니다. 각 문자의 ascii 코드8비트의 이진수로 변경한 후 6비트씩 잘라서 다시 10진수로 변환합니다. 그 다음 base64 색인표에서 해당하는 문자로 변경하면 끝입니다. 8과 6의 최소 공배수는 24이므로 24비트, 즉 3글자씩 잘라서 4글자의 base64 인코딩 문자를 만들어냅니다. 6비트씩 자르기 때문에 색인표에는 0 ~ 63까지 총 64개의 값이 들어있습니다. 색인표에 따르면 Boy를 base64 인코딩 했을 때 Qm95 가 됩니다. 

출처: 위키피디아

base64 인코딩은 3글자씩 잘라서 인코딩 하기 때문에 글자수가 3으로 나눠 떨어지지 않으면 padding 값을 집어 넣어줍니다. 6비트씩 잘랐을 때 부족한 비트는 0으로 채워줍니다. 

색인표에서 값을 찾아보면 Hi를 base64 인코딩 했을 때 SGk= 가 된다는 것을 알 수 있습니다. base64 인코딩을 해주는 javascript 내장함수는 btoa() 함수를 이용해 결과가 맞는지 확인해보겠습니다.

btoa() 함수를 이용한 base64 인코딩

그럼 이제 base64Url 인코딩에 대해 알아보겠습니다. base64 색인표의 62, 63번의 값을 보면 + 와 / 입니다. 이 문자들은 URL에 사용되는 문자들이기 때문에 base64 인코딩 된 문자열이 URL에 들어가면 예기치 못한 오류가 생길 수 있습니다. 그래서 62, 63번의 값을 각각 - 와 _ 로 바꾼 것이 base64Url 인코딩입니다. 

다시 한번 정리하면, base64Url 인코딩base64 인코딩 문자열이 URL에 사용될 수 있도록 변경한 인코딩 방법입니다. 따라서 인코딩 문자열에 +, / 나 -, _ 포함되어 있지 않다면 base64 인코딩base64Url 인코딩의 결과값은 같습니다.

이제 base64Url 인코딩에 대해 알았으니 실제 JWT의 header와 payload에 어떤 정보가 담겨있는지 알아보겠습니다.

위에 적혀있던 JWT를 다시 옮겨 적겠습니다.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9       // header
.eyJuYW1lIjoiamV3b29rIiwiaWF0IjoxNjA5Mzg5Mzg3LCJleHAiOjE2MDk1NjIxODd9     // payload
.hNZyY46gm0mXOgz7gNgDfzDDuTqAA0w5gRbqo6jqx3o     // signature

header는 base64Url 인코딩 된 문자열인데 - 와 _ 가 포함되어 있지 않으니 base64 인코딩 한 결과값과 같습니다. 따라서 base64 디코딩을 해주는 atob() 함수를 통해 원래 어떤 문자열이었는지 확인할 수 있습니다.

atob() 함수를 통한 header base64 디코딩

atob 함수를 실행시켜보니 header가 아래와 같은 정보가 담겨있다는 것을 확인했습니다.

{
    alg: "HS256",
    typ: "JWT",
}

보통 header는 서명 알고리즘을 뜻하는 alg(algorithm)와 토큰의 타입를 뜻하는 typ(type)로 이루어져 있습니다. JWT의 크기를 줄이기 위해 자주 사용되는 필드들은 세 글자로 표현한다고 합니다. 이 header 정보를 통해 이 토큰의 서명 알고리즘이 HS256이며 토큰의 타입이 JWT 라는 것을 알 수 있습니다.

다음은 payload를 base64 디코딩을 하여 어떤 정보가 들어있는지 살펴보겠습니다.

atob() 함수를 통한 payload base64 디코딩

payload 역시 header와 마찬가지로 - 와 _ 가 포함되어 있지 않기 때문에 atob() 함수를 통해 디코딩을 실행했고 아래와 같은 정보가 담겨져 있다는 것을 확인했습니다.

{
    name: "jewook",
    iat: 1609389387,
    exp: 1609562187,
}

payload 역시 세 글자로 이루어진 필드들이 있습니다. payload의 필드들은 claim이라고 부릅니다. iat(Issued At) claim에는 토큰 생성시간의 unix time, exp(Expiration Time) claim에는 토큰 만료시간의 unix time 값이 들어있습니다. iat, exp claim을 통해 이 토큰은 iat 와 exp 사이의 시간에서만 유효한 토큰이라는 사실을 알 수 있습니다. 이외에도 자주 사용되는 payload claim들은 여기서 볼 수 있습니다.

자주 사용되는 payload claim 외에도 임의의 claim을 추가할 수 있습니다. 저는 name이라는 claim을 넣어 이 토큰이 "jewook" 이라는 사용자의 토큰이라는 정보를 넣어놨습니다. 그렇기 때문에 서버에서 토큰의 payload를 보고 이 토큰이 포함된 request가 누구에게로부터 온 request인지 알 수 있는 것입니다. 

마지막으로 signature를 살펴보겠습니다.

header와 payload는 암호화 된 문자열이 아닌 base64Url 인코딩 된 문자열입니다. 따라서 토큰을 가지고 있다면 누구나 header와 payload의 값을 위, 변조 할 수 있습니다. 그래서 해당 토큰이 서버로부터 생성되었고 제 3자에 의해 위, 변조 되지 않았음을 증명하기 위한 signature 부분이 필요합니다.

header의 alg에 들어있는 알고리즘으로 signature가 생성됩니다. 위 예시 코드의 alg값은 HS256이므로 HS256으로 설명하겠습니다. 

HS256을 이용한 signature 생성 방식은 아래와 같습니다.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

HS256은 HMAC-SHA256 이라는 암호화 알고리즘을 의미합니다. HMAC-SHA256은 대칭키(비밀키) 암호화 알고리즘입니다.

header와 payload를 각각 base64Url 인코딩 한 후 .으로 연결한 문자열을 secret이라고 불리는 대칭키(비밀키)를 가지고 암호화하여 signature를 생성합니다. secret서버에만 저장됩니다.

바로 이 signature 덕분에 JWT를 이용하면 서버측에 session 저장소와 같은 별도의 저장공간이 필요없게 됩니다. 위에서 JWT는 위, 변조가 가능하다고 설명했습니다. 하지만 JWT에 포함된 signature 덕분에 위, 변조를 감지할 수 있습니다.

서버에서 signature를 생성할 때 header, payload의 내용을 서버에만 저장되어 있는 secret으로 암호화하여 생성합니다. 그리고 서버에서 JWT가 포함된 request를 받았을 때 해당 JWT header의 alg에 적힌 알고리즘secret을 통해 signature를 복호화 합니다. 복호화 된 signature 안에 들어있는 header, payload의 내용과 실제 header, payload의 내용이 일치해야만 서버는 해당 토큰을 정상적인 토큰으로 판단합니다. 정상적인 토큰이 아니라고 판단한 경우에는 해당 토큰이 들어있는 request를 무시해버립니다.

클라이언트에 저장된 JWT를 위, 변조 하더라도 secret은 서버에만 저장되어 있기 때문에 올바른 signature를 생성할 수 없습니다. 따라서 JWT위, 변조에 대해 안전하다고 볼 수 있습니다. secret만 서버에 저장해놓으면 JWT가 올바른 JWT 인지 확인할 수 있기 때문에 session 저장소와 같은 서버에 부담을 줄 수  있는 별도의 저장소 없이 인증을 구현할 수 있는 것입니다.

'Web' 카테고리의 다른 글

HTTPS는 왜 안전한가?  (0) 2020.11.28
HTTP에 대해서(2)  (0) 2020.01.25
HTTP에 대해서(1)  (0) 2020.01.21
Express로 만든 웹 사이트 heroku에 올리기  (0) 2019.08.18