Gidhub BE Developer

JWT 소개 및 구조

2018-12-04
goodGid

JWT란?

  • JSON Web Token(JWT)은 웹표준(RFC 7519)으로서

  • 두 개체에서 JSON 객체를 사용하여 가볍고 자가수용적인(self-contained) 방식으로 정보를 안전성 있게 전달해준다.

  • 수많은 프로그래밍 언어에서 지원된다.

    • JWT는 대부분의 프로그래밍 언어에서 지원된다.

    • C, Java, Python, C++, R, C#, PHP, JavaScript, Ruby, Go, Swift 등등

  • 자가 수용적 (self-contained)이다.

    • JWT는 필요한 모든 정보를 자체적으로 지닌다.

    • JWT 시스템에서 발급된 토큰은 토큰에 대한 기본 정보전달 할 정보(로그인시스템에서는 유저 정보) 그리고 토큰이 검증 됐다는것을 증명해주는 signature를 포함한다.

  • 쉽게 전달 될 수 있다.

    • JWT는 자가수용적이므로 두 개체 사이에서 손쉽게 전달될 수 있다.

    • 웹서버의 경우 HTTP의 헤더에 넣어서 전달 할 수도 있고 URL의 파라미터로 전달 할 수 있다.


사용되는 경우

회원 인증

  • JWT를 사용하는 가장 흔한 시나리오이다.

  • 유저가 로그인을 하면 서버는 유저의 정보에 기반한 토큰을 발급하여 유저에게 전달해준다.

  • 그 후 유저가 서버에 요청을 할 때 마다 JWT를 포함하여 전달한다.

  • 서버는 클라이언트에게서 요청을 받을때 마다 해당 토큰의 유효성을 검토하고

  • 유저가 요청한 작업에 권한이 있는지 확인하여 작업을 처리합니다.

  • 또한 서버측에서는 유저의 세션을 유지 할 필요가 없다.

  • 즉 유저가 로그인되어있는지 안되어있는지 신경 쓸 필요가 없고

  • 유저가 요청을 했을때 토큰만 확인하면 되니 세션 관리가 필요 없어서 서버 자원을 많이 아낄 수 있다.

정보 교류

  • JWT는 두 개체 사이에서 안정성있게 정보를 교환하기에 좋은 방법이다.

  • 그 이유는 정보가 sign이 되어있기 때문에 정보를 보낸이가 바뀌진 않았는지 또 정보가 도중에 조작되지는 않았는지 검증할 수 있다.


JWT 구조

JWT 는 . 을 구분자로 3가지의 문자열로 되어있습니다. 구조는 다음과 같이 이루어져있습니다:

헤더(Header)

  • Header는 두가지의 정보를 지니고 있다.

  • typ: 토큰의 타입을 지정한다.

  • alg: 해싱 알고리즘을 지정한다.

    • 해싱 알고리즘으로는 보통 HMAC SHA256 혹은 RSA가 사용되며

    • 이 알고리즘은 토큰을 검증 할 때 사용되는 signature 부분에서 사용된다.

{
  "typ": "JWT",
  "alg": "HS256"
}
  • 이 정보를 base64로 인코딩을 하면 다음과 같다.
const header = {
  "typ": "JWT",
  "alg": "HS256"
};

// encode to base64
const encodedPayload = new Buffer(JSON.stringify(payload))
                            .toString('base64')
                            .replace('=', '');
console.log('payload: ',encodedPayload);

/* 
header: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
*/

정보(payload)

  • Payload 부분에는 토큰에 담을 정보가 들어있다.

  • 여기에 담는 정보의 한 조각을 클레임(claim)이라고 부르고

  • 이는 Name / Value의 한 쌍으로 이뤄져있다.

  • 토큰에는 여러개의 클레임들을 넣을 수 있다.


  • 클레임의 종류는 다음과 같이 크게 세 분류로 나뉘어있다.
  1. 등록된 (registered) 클레임

  2. 공개 (public) 클레임

  3. 비공개 (private) 클레임

등록된(registered) 클레임

  • 등록된 클레임들은 서비스에서 필요한 정보들이 아닌

  • 토큰에 대한 정보들을 담기위하여 이름이 이미 정해진 클레임들이다.

  • 등록된 클레임의 사용은 모두 선택적 (optional)이며 이에 포함된 클레임 이름들은 다음과 같다.

    • iss: 토큰 발급자 (issuer)

    • sub: 토큰 제목 (subject)

    • aud: 토큰 대상자 (audience)

    • exp: 토큰의 만료시간 (expiraton)
      • 시간은 NumericDate 형식으로 되어있어야 하며(예: 1480849147370)
      • 언제나 현재 시간보다 이후로 되어야 한다.
    • nbf: Not Before를 의미하며 토큰의 활성 날짜와 비슷한 개념이다.
      • 여기에도 NumericDate 형식으로 날짜를 지정하며
      • 이 날짜가 지나기 전까지는 토큰이 처리되지 않는다.
    • iat: 토큰이 발급된 시간 (issued at)
      • 이 값을 사용하여 토큰의 age가 얼마나 되었는지 판단할 수 있다.
    • jti: JWT의 고유 식별자
      • 주로 중복적인 처리를 방지하기 위하여 사용된다.
      • 일회용 토큰에 사용하면 유용하다.

공개(public) 클레임

  • 공개 클레임들은 충돌이 방지된(collision-resistant) 이름을 가지고 있어야 한다.

  • 충돌을 방지하기 위해서는 클레임 이름을 URI 형식으로 짓는다.

{
    "https://goodgid.github.io/jwt_claims/is_admin": true
}

비공개(private) 클레임

  • 등록된 클레임도 공개된 클레임들도 아니다.

  • 양 측간에(보통 클라이언트 <-> 서버) 협의하에 사용되는 클레임 이름들이다.

  • 공개 클레임과는 달리 이름이 중복되어 충돌이 될 수 있으니 주의해서 사용해야한다.

{
    "username": "goodgid"
}

예제

{
    "iss": "goodgid.com",
    "exp": "1485270000000",
    "https://goodgid.github.io/jwt_claims/is_admin": true,
    "userId": "11028373727102",
    "username": "goodgid"
}
  • 위 예제 payload는 2개의 등록된 클레임 1개의 공개 클레임 2개의 비공개 클레임으로 이뤄져있다.

  • 위 데이터를 base64로 인코딩하면 다음과 같다.

const payload = {
    "iss": "goodgid.com",
    "exp": "1485270000000",
    "https://goodgid.github.io/jwt_claims/is_admin": true,
    "userId": "11028373727102",
    "username": "goodgid"
};

// encode to base64
const encodedPayload = new Buffer(JSON.stringify(payload))
                            .toString('base64')
                            .replace('=', '');

console.log('payload: ',encodedPayload);

/* 
payload:  eyJpc3MiOiJ2ZWxvcGVydC5jb20iLCJleHAiOiIxNDg1MjcwMDAwMDAwIiwiaHR0cHM6Ly92ZWxvcGVydC5jb20vand0X2NsYWltcy9pc19hZG1pbiI6dHJ1ZSwidXNlcklkIjoiMTEwMjgzNzM3MjcxMDIiLCJ1c2VybmFtZSI6InZlbG9wZXJ0In0
*/

서명(signature)

  • JSON Web Token의 마지막 부분은 바로 서명(signature)이다.

  • 이 서명은 헤더의 인코딩값정보의 인코딩값을 합친 후 주어진 비밀키로 해쉬를 하여 생성한다.

  • 서명 부분을 만드는 수도코드(pseudocode)의 구조는 다음과 같다.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
  • 이렇게 만든 해쉬를 base64 형태로 나타내면 된다.

  • 문자열을 인코딩 하는게 아닌 hex → base64 인코딩을 해야한다.


  • 최종적으로 JWT 토큰을 발급해보자.

  • 위에서 헤더와 정보의 인코딩 값 사이에 .을 넣어주고 합친다.

  • 이 값을 비밀키의 값을 secret으로 해싱을 하고 base64로 인코딩하면 다음과 같은 값이 나온다.
const crypto = require('crypto');
const signature = crypto.createHmac('sha256', 'secret')
             .update(encodedHeader + '.' + encodedPayload)
             .digest('base64')
             .replace('=', '');

console.log('signature: ',signature);

/* 
signature: WE5fMufM0NDSVGJ8cAolXGkyB5RmYwCto1pQwDIqo2w
*/
  • 그러면 지금까지 구한 값들을 .을 중간자로 다 합쳐주면 하나의 토큰이 완성된다.

  • 이 값을 https://jwt.io/의 붙여넣으면 값을 확인할 수 있다.

Reference


Index