JSON Web Token 소개

Bruce Han·2022년 1월 29일
2
post-thumbnail

개요

인증은 웹, 모바일 등 거의 모든 애플리케이션에서 가장 중요한 부분 중 하나입니다. 인증에는 흔히들 알고 있는 쿠키, 세션, 토큰 기반 인증이 있겠죠.
토큰을 중점적으로 소개하려고 합니다. 조연으로 세션도 나옵니다 😀


이번 포스팅에서는

  • 세션 기반 인증 vs 토큰 기반 인증 (JWT가 왜 나타났는지)
  • JWT의 운용 방식
  • JWT의 구성 요소 및 만들어지는 과정
  • 우리의 앱을 어떻게 지키는지, JWT의 유효성 인증

에 대해서 알아보겠습니다.


웹, 모바일 등 어느 애플리케이션을 사용하던, 앱의 기능에 액세스하기 위한 계정이 필요합니다. 이런 일련의 과정을 인증(Authentication)이라 합니다.

그러면, 본인 계정을 어떻게 인증할까요?
먼저, 과거 웹사이트에서 많이 쓰이던 간단한 방식에 대해 알아겠습니다.

이름하여 세션 기반 인증이 되겠습니다.

위의 이미지에서 알 수 있듯이, 사용자가 웹사이트에 로그인 하면 서버는 메모리/데이터베이스에다가 해당 사용자에 대한 세션을 생성하고 저장합니다. 또한, 서버는 클라이언트가 브라우저 쿠키🍪에 저장하도록 세션 ID를 반환합니다.

서버의 세션에는 유효기간이 있습니다. 그 시간이 지나면, 세션은 만료되며 유저는 다른 세션을 만들기 위한 로그인을 다시 해야합니다.

만약, 유저가 로그인을 했고 그 세션이 아직 만료가 되지 않았으면, 세션 ID를 포함한 쿠키는 서버에 대한 모든 HTTP 요청과 함께 이동합니다. 즉, 모든 HTTP 요청에는 쿠키가 달려있다는 것입니다.
이때, 서버는 이 세션 ID를 저장되어있는 세션과 비교하여 해당하는 응답을 인증하고 반환합니다.


그러면, 왜 세션 인증 방식보다는 토큰 인증 방식을 요구하게 된 걸까요?
라는 질문에는 요구하는 곳이 웹사이트만 있는 게 아니고 다른 많은 플랫폼도 있으니 그런 것이라고 답할 수 있겠습니다.

세션과 함께라면🍜 잘 돌아가는 웹사이트가 있다고 가정해봅시다.
하루는, 모바일(네이티브 앱)용 시스템을 구현하기 위해 현재 웹 앱과 동일한 데이터베이스를 사용하고자 합니다. 뭘 해야 할까요?

세션 기반 인증을 사용하는 네이티브 앱은 쿠키가 없기 때문에 사용자를 인증할 수 없습니다.

굳이 네이티브 앱을 지원하는 또 다른 프로젝트를 만들거나 네이티브 앱 유저들을 위한 인증 모듈을 사용할 필요는 없습니다.
이러한 이유에서 토큰 기반 인증이 나온 것입니다.

이 방법을 사용하면 인코딩된 사용자 정보가 서버에 의해 JSON Web Token(JWT)안에 포함되면서 클라이언트로 보내집니다. 오늘날의 많은 REST한 API들(RESTful API)이 이러한 방법을 사용합니다.


JWT는 어떻게 운용되는가

위 그림에서도 알 수 있듯이, 서버가 세션을 만드는 대신 유저가 로그인한 그 데이터로부터 JWT를 만들고 클라이언트에다가 JWT를 보냅니다. 클라이언트는 받은 그 때부터 JWT를 저장하는데, 매 클라이언트의 요청마다 헤더에 얹어있는 JWT(보통 헤더에 있음)가 붙어있어야 합니다.
서버는 그 JWT의 유효성을 검사하고, 검사 결과에 따라 응답을 반환합니다.

클라이언트단에서는 사용하는 플랫폼에 따라 저장하는 곳이 다릅니다.

에다가 저장합니다.


JWT의 구성 요소

먼저, JWT는

  • Header
  • Payload
  • Signature

이렇게 3가지로 구성되어 있습니다.

Header(헤더, 그 브라우저 헤더와는 다른 것)

헤더는 서명 생성을 위해서 어느 알고리즘을 사용할지 식별합니다.
JWT를 어떻게 계산하지?라는 질문에 대한 답이 되겠습니다.

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

위 코드에서 alg는 'algorithm'을 나타내며, 토큰 서명을 생성하기 위한 해시 알고리즘을 나타냅니다.
HS256은 이 토큰이 Secret Key를 사용하는 HMAC-SHA256이라는 암호화 알고리즘을 사용하여 서명됨을 의미합니다.

Payload

페이로드는 일련의 클레임(요구)을 포함합니다. JWT 사양은 토큰에 일반적으로 포함되는 표준 필드인 7개 등록 클레임 이름(Registered Claim Names)을 정의합니다.

{
  "userId" : "bruce han",
  "username" : "Bruce Han",
  "email" : "contact@brucehan.com",
  // standard fields
  "iss" : "Bruce Han",
  "iat" : 1570238918,
  "exp" : 1570238992
}

본디 JSON에는 주석처리가 안 됩니다.

이런 식으로, 주석을 기준으로 위의 3개는 사용자가 직접 지정한 필드, 밑에 3개는 표준 필드입니다. 꼭 몇 개를 써야 된다, 표준은 꼭 넣어야 한다 이런 건 없고 본인이 해결할 문제에 대해서 필요한 것만 뽑아가시면 되겠습니다.(예: 로그인할 때 유저 이름은 갖고 가지만 비밀번호는 산출을 안 한다던지 등)

저 밑에 3개의 표준은 다음과 같습니다.

  • iss (Issuer) : JWT를 발행한 사람
  • iat (Issued At Time) : JWT가 발행된 시간
  • exp (Expiration Time) : JWT 만료 시간

표준 필드에 대한 더 자세한 내용은 여기를 참고하세요

Signature

해쉬 알고리즘을 사용하는 부분입니다. 이 서명(Signature) 부분을 통하여 토큰을 안전하게 확인할 수 있습니다. 서명은 Base64url 인코딩을 이용하여 헤더와 페이로드를 인코딩하고, 이 둘을 점(.) 구분자로 연결시킴으로써 계산됩니다.

아래 Signature를 가져오는 코드를 보시면

const data = Base64UrlEncode(header) + '.' + Base64UrlEncode(payload);
const hashedData = Hash(data, secret);
const signature = Base64UrlEncode(hashedData);

이런 식으로 서명을 가져옵니다.
위에서 이미 설명을 드렸지만, 먼저 Header와 Payload를 점으로 연결합니다.

data = '[encodedHeader].[encodedPayload]'

그 다음, 비밀키로 헤더에 정의되어 있는 해쉬 알고리즘을 사용하는 해쉬를 만듭니다.
마지막으로, Signature를 얻기 위해 해싱 결과를 인코딩합니다.


결합하기

Header, Payload, Signature 를 얻은 후, 그것들을 JWT의 표준 구조로 결합합니다 : header.payload.signature 이렇게요.

코드로 결합하는 과정을 보여 드리겠습니다.

const encodedHeader = base64urlEncode(header);
/* Result */ 
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"

const encodedPayload = base64urlEncode(payload);
/* Result */
"eyJzdWIiOiIwOTg3NjU0MzIxIiwibmFtZSI6IkJydWNlIEhhbiIsImlhdCI6MTUxNjIzOTAyMn0"

const data = encodedHeader + "." + encodedPayload;
const hashedData = Hash(data, secret);
const signature = base64urlEncode(hashedData);
/* Result */
"ID18A39H8vUWb8sBYC2LyvbuKfshSwPQaMAbwJvr1Ac"

// header.payload.signature
const JWT = encodedHeader + "." + encodedPayload + "." + signature;
/* Result */
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwOTg3NjU0MzIxIiwibmFtZSI6IkJydWNlIEhhbiIsImlhdCI6MTUxNjIzOTAyMn0.ID18A39H8vUWb8sBYC2LyvbuKfshSwPQaMAbwJvr1Ac"

우리의 데이터를 JWT로 보호하는 법?

출처 : https://namu.wiki/w/%EA%B7%B8%EB%9F%B0%20%EA%B1%B0%20%EC%97%86%EB%8B%A4

JWT는 데이터를 숨기거나, 모호하게 하거나, 보호하지는 않습니다. 전혀요.
여러분은 단지 JWT(Header, Payload, Signature)를 생성하는 과정은 데이터를 암호화하지 않고 암호화 및 해시 데이터만 생성하는 것을 볼 수 있을 뿐이죠.
JWT의 목적은 단지 데이터가 확실한 소스에 의해 생성되었음을 증명하기 위함이랍니다.

그래서,
만약에말야 중간자 공격(Man in the middle attack)으로 JWT를 얻을 수 있다면, 사용자 정보를 디코딩해야 할까요?
네,,, 뭐,,, 그렇다고 합니다...
그렇게 될 수 있기에 애플리케이션에 HTTPS 암호화가 있는지 항상 확인해야 한답니다.


서버가 클라이언트로부터 온 JWT의 유효성을 판별하는 법?

위에서 Signature를 만들기 위해 비밀키를 사용한다고 했었죠. 이 비밀키는 모든 애플리케이션에서 고유하고 서버 측에 안전하게 저장되어야 합니다.

클라이언트로부터 JWT를 받을 때, 서버는 Signature를 얻고, Signature가 해쉬 알고리즘과 비밀키에 의해 정상적으로 해쉬되었는지 판별합니다. 만약 서버의 Signature와 맞으면, (현재 가지고 있는)JWT는 유효하다고 보면 됩니다.

그러면

서버로 JWT를 보낼 때 페이로드 정보를 추가하거나 편집하고 싶을 때에는 어떻게 하면 좋을까요?
클라이언트에게 보내기 전에 토큰을 저장하는 방법도 있습니다. 그러면 나중에 클라이언트에 의해 전송된 JWT가 유효한지에 대해서는 확신할 수 있겠죠.
게다가, 서버에 사용자의 토큰을 저장하면 시스템에 의해 강제 로그아웃도 할 수 있습니다.


마치며

인증을 위한 방법에는 사실 제일 좋은 건 없다고 합니다. 본인이 직면한 문제에 대해서 사용했을 때 적합한 것들이 있기 때문에(a.k.a. 케이스 바이 케이스) 무적권 사용해야 한다! 라는 방법은 없다고 봅니다.
그러나, 많은 사용자들이 사용하는 거대한 크기에 사용될 앱에는 JWT 인증 방식이 클라이언트 측에서 토큰이 저장된다는 면에서 적합할 수도 있습니다.

글을 봐주신 분들께 압도적 감사.
지적을 해주시는 분들께 압도적 감사.
하트 눌러주시면 압도적 감사.

참고

profile
만 가지 발차기를 한 번씩 연습하는 사람은 두렵지 않다. 내가 두려워 하는 사람은 한 가지 발차기를 만 번씩 연습하는 사람이다. - Bruce Lee

0개의 댓글