본문 바로가기
개발

JWT 겉핥기

by 상5c 2024. 2. 4.

회사에서 마주치게 되는 기술들을 가볍게 익히고 조금씩 깊이 들어가는 순으로 공부해보려 한다. 이 글에서는 왜 쓰는지, 어떤 장단점이 있는지보다는 어떻게 쓰는지에 초점을 맞춰 글을 작성한다.

 

JWT란?

  • JSON Web Token의 약자로, 정보를 안전하게 전송하기 위한 토큰 기반 인증 및 권한 부여 개방형 표준(RFC 7519)이다. 이름에서 알 수 있듯, JSON 형태로 이루어진 토큰이다.
  • 종종 JWT 토큰이라고 읽기도 하는데 올바른 표현은 아니다. (JSON Web Token Token이라는 의미가 됨)

 

구성

  • JWT는 헤더(Header), 페이로드(Payload), 서명(Signature) 세 가지 부분으로 구성된다. 세 부분을 하나로 이어 하나의 문자열을 만든다. 최종 형태는 xxx.yyy.zzz 형태가 된다. (헤더.페이로드.서명)
  • 각각의 부분에 담기는 데이터의 키는 3글자를 많이 사용한다.

 

헤더 (Header)

{
  "alg": "HS256",
  "typ": "JWT"
}
  • JWT 유형 및 서명 알고리즘에 대한 정보를 담는다. JSON 문자열을 Base64Url 인코딩하면 JWT 문자열의 첫 부분(xxx)을 담당하는 문자열이 된다.
    • 참고로, 인코딩은 암호화가 아니다. 인코딩은 데이터를 숨기는데 목적을 두지 않는다.
  • alg: Algorithm의 약자로, 토큰 서명 알고리즘을 나타낸다. HS256은 HMAC SHA 256을 나타내고, 이외에도 RSA, ECDSA가 사용될 수 있다.
  • type: Type의 약자로 토큰의 타입을 나타낸다.

헤더를 자바 코드로 만들어보면 다음과 같다.


@Test
void crateHeader() {
    String header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";

    String encodedHeader = Base64.getUrlEncoder()
            .withoutPadding()
            .encodeToString(header.getBytes(StandardCharsets.UTF_8));

    System.out.println("Encoded JWT Header: " + encodedHeader); // Encoded JWT Header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
}

 

페이로드 (Payload)

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
  • 애플리케이션이 사용하는 데이터가 담기는 부분이며 각 값을 클레임(claims)이라 한다. 클레임에는 등록된 클레임, 공개 클레임, 비공개 클레임이 있다. 다른 서비스와의 충돌 방지가 목적이다.
  • 페이로드도 헤더와 마찬가지로, 데이터를 Base64Url 인코딩을 수행하면 된다.
    • 한 번 더 주의사항으로, 암호화가 아니다. 이어지는 서명을 통해 변조로부터 보호되나, 누구나 읽을 수 있는 값이다. 암호화가 필요하다면 별도로 수행해야 한다.
  • (헤더와 같은 과정을 거치기에 코드는 생략한다)

 

서명 (Signature)

  • 인코딩 된 헤더와 인코딩된 페이로드를 점(”.”)으로 이어 하나의 문자열로 만든 후, 헤더에 지정한 알고리즘을 사용하여 서명한 값이 담긴다.
HMACSHA256(encodedHeader + "." + encodedPayload, secret)
  • 이렇게 만든 서명을 점으로 이어 하나의 문자열로 만들면 JWT가 된다.
  • 간단히 다시 정리하자면
    • 헤더를 인코딩한 결과물 = A
    • 페이로드를 인코딩한 결과물 = B
    • 서명 = A + “.” + B 를 서명한 결과물 = C
    • 최종 JWT의 형태 → A.B.C

 

자바 코드

설명을 구현한 자바 코드는 아래와 같다.


@Test
void jwt() throws NoSuchAlgorithmException, InvalidKeyException {
    String header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
    String payload = "{\"sub\":\"1234567890\",\"name\":\"John Doe\",\"admin\":true}";

    String encodedHeader = getBase64UrlEncoded(header.getBytes(StandardCharsets.UTF_8));
    System.out.println("헤더: " + encodedHeader);

    String encodedPayload = getBase64UrlEncoded(payload.getBytes(StandardCharsets.UTF_8));
    System.out.println("페이로드: " + encodedPayload);

    // HMAC SHA256으로 서명 생성
    String secret = "your-256-bit-secret"; // 서명을 위한 비밀키
    SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
    Mac sha256Hmac = Mac.getInstance("HmacSHA256");
    sha256Hmac.init(keySpec);

    String data = encodedHeader + "." + encodedPayload;
    byte[] signatureBytes = sha256Hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));
    String signature = getBase64UrlEncoded(signatureBytes);
    System.out.println("서명: " + signature);

    String jwt = encodedHeader + "." + encodedPayload + "." + signature;
    System.out.println("JWT: " + jwt);
}

private String getBase64UrlEncoded(byte[] bytes) {
    return Base64.getUrlEncoder()
            .withoutPadding()
            .encodeToString(bytes);
}

 

마무리

인터넷에 올라온 대부분 예시는 jjwt 라이브러리를 사용했는데, 이 글에서는 java 기본 라이브러리만을 사용했다.
JWT를 생성하는 입장에서 작성된 글이며, 사용하는 입장에서는 데이터를 검증하고 사용하면 된다.

 

참고