회사에서 마주치게 되는 기술들을 가볍게 익히고 조금씩 깊이 들어가는 순으로 공부해보려 한다. 이 글에서는 왜 쓰는지, 어떤 장단점이 있는지보다는 어떻게 쓰는지에 초점을 맞춰 글을 작성한다.
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를 생성하는 입장에서 작성된 글이며, 사용하는 입장에서는 데이터를 검증하고 사용하면 된다.