JWT의 구조: 헤더.페이로드.서명
JWT(JSON Web Token, RFC 7519)는 점(.)으로 이어진 세 덩어리입니다. 앞의 두 덩어리는 JSON을 Base64URL로 인코딩한 것이고, 세 번째는 서명입니다. 이 페이지의 데모 토큰을 나눠 보면 다음과 같습니다.
| 부분 | 내용 | 데모 값 (Base64URL) |
|---|---|---|
| 헤더 | 알고리즘(alg), 타입(typ) 등 메타데이터 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 = {"alg":"HS256","typ":"JWT"} |
| 페이로드 | 클레임(sub, iat, exp, 사용자 정의 값) | eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iu2Zjeq4uOuPmSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAzNjAwfQ |
| 서명 | "헤더.페이로드" 문자열에 대한 HMAC 또는 전자서명 | h3A987M6nrZ30134c0r8JMhI-ZWzw-bFETtqHAVm5sw |
서명 대상은 정확히 Base64URL(헤더) + "." + Base64URL(페이로드) 문자열입니다. 서명이 가리키는 것은 JSON의 의미가 아니라 인코딩된 바이트 그대로이므로, 공백 하나만 달라도 서명은 달라집니다. HS256의 서명은 이 문자열에 비밀키로 HMAC-SHA-256을 계산한 결과입니다. 위 데모 토큰은 빌드할 때 비밀키 playinfobox-demo-secret으로 실제 계산해 넣은 값이라, 위 "서명 검증" 버튼으로 통과를 확인할 수 있습니다.
Base64URL은 일반 Base64와 무엇이 다른가
표준 Base64는 URL이나 헤더에서 문제가 되는 +, /, 끝의 = 패딩을 씁니다. Base64URL은+를 -로, /를 _로 바꾸고 패딩을 생략합니다. 예를 들어 바이트 0xFB 0xFF는 Base64로 +/8=, Base64URL로 -_8입니다. 그래서 JWT를 일반 Base64 디코더에 넣으면 -, _ 때문에 실패하거나 글자가 깨질 수 있습니다. 이 도구는 문자를 되돌리고 길이가 4의 배수가 되도록 =를 채운 뒤 UTF-8로 해석합니다. 일반 Base64 인코딩·디코딩은 Base64 변환기에서 할 수 있습니다.
디코딩은 검증이 아닙니다
페이로드는 암호화가 아니라 인코딩입니다. 토큰을 가진 사람은 누구나 내용을 읽을 수 있으므로 비밀번호, 주민등록번호, 카드번호 같은 값을 넣으면 안 됩니다. 또한 디코딩해서 이름이나 권한이 보인다는 사실은 그 토큰이 진짜라는 증거가 아닙니다. 페이로드는 누구나 마음대로 써서 인코딩할 수 있기 때문입니다. 신뢰 여부는 서명 검증으로만 판단합니다. 서버(API)는 요청이 올 때마다 다음을 모두 확인해야 합니다.
- 서명이 올바른가: 서버가 미리 정한 알고리즘과 키로 검증합니다.
- exp(만료), nbf(유효 시작), iss(발급자), aud(대상)가 기대값과 맞는가.
- 필요한 권한(scope/roles)이 있는가. 클라이언트가 읽는 클레임은 UI 편의용일 뿐 권한 판단 근거가 아닙니다.
확실히 알아야 할 보안 주의점
- alg: none: 헤더에
{"alg":"none"}를 쓰면 서명 없는 토큰(Unsecured JWT)이 됩니다(예: 헤더 인코딩eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0, 서명 부분은 빈 문자열). RFC 7519가 정의한 형식이지만, 서명을 요구하는 API가 토큰 헤더의 alg 값을 그대로 믿고 검증을 생략하면 누구나 토큰을 위조할 수 있습니다. 서버는 허용 알고리즘 목록을 직접 고정하고 none을 거부해야 합니다. - 알고리즘 혼동: RS256(공개키)으로 발급하는 서비스에서 공격자가 헤더를 HS256으로 바꾸고 공개키를 HMAC 비밀키처럼 쓰게 만드는 공격이 알려져 있습니다. 검증 라이브러리에 키와 함께 기대 알고리즘을 명시하세요.
- 만료 없는 토큰: exp가 없으면 탈취된 토큰이 영구히 유효할 수 있습니다. 액세스 토큰은 짧은 수명(분~시간 단위)으로, 갱신은 별도 리프레시 토큰으로 설계하는 것이 일반적입니다.
- 약한 HMAC 비밀키: HS256은 비밀키를 추측해 오프라인으로 대입하는 공격에 노출됩니다. 키는 알고리즘 출력 길이 이상(HS256이면 32바이트 이상)의 무작위 값을 쓰세요. 생성에는 UUID·랜덤 토큰 생성기의 HEX 토큰이나 비밀번호 생성기가 쓸 만합니다.
- 토큰 보관: localStorage에 두면 XSS로 읽히고, 쿠키에 두면 CSRF를 고려해야 합니다. 어느 쪽이든 완벽하지 않으므로 HttpOnly·Secure·SameSite 쿠키 등 선택지를 위협 모델에 맞춰 비교하세요.
- 로그와 화면 공유: 운영 토큰은 만료 전까지 비밀번호처럼 쓰일 수 있습니다. 이슈 트래커나 채팅에 붙여넣지 마세요.
서명 길이로 알고리즘 짐작하기
서명 부분의 길이는 알고리즘이 정합니다. 바이트 수 n의 Base64URL(패딩 없음) 문자 수는 ceil(4n/3)입니다. 이 도구는 서명 길이를 계산해 헤더의 alg와 맞는지 알려 줍니다.
| alg | 방식 | 서명 바이트 | Base64URL 문자 수 |
|---|---|---|---|
| HS256 | HMAC-SHA-256 | 32 | 43 |
| HS384 | HMAC-SHA-384 | 48 | 64 |
| HS512 | HMAC-SHA-512 | 64 | 86 |
| ES256 | ECDSA P-256 (r‖s 고정 길이) | 64 | 86 |
| ES384 | ECDSA P-384 | 96 | 128 |
| ES512 | ECDSA P-521 | 132 | 176 |
| RS256 | RSASSA-PKCS1-v1_5, 2048비트 키 | 256 | 342 |
RS256의 서명 길이는 키 크기와 같으므로 3072비트 키라면 384바이트입니다. 표의 값은 2048비트 기준입니다. ES256 등 ECDSA의 JWT 서명은 DER가 아니라 r과 s를 이어 붙인 고정 길이 형식(RFC 7518)입니다.
등록 클레임: exp, nbf, iat 읽는 법
시간 클레임은 1970-01-01 00:00:00 UTC 기준 초 단위의 정수(NumericDate)입니다. 밀리초가 아닙니다. 그래서 값이 13자리 정도로 크면(1,000,000,000,000 이상) 밀리초를 잘못 넣은 토큰일 가능성이 높고, 이 도구는 이를 경고합니다. 변환 원리는 Unix 타임스탬프 변환기에서 직접 확인해 볼 수 있습니다.
| 클레임 | 의미 | 판정 기준 |
|---|---|---|
| iss | 발급자 | 기대하는 발급자와 일치해야 함 |
| sub | 주체(사용자 ID 등) | 발급자 범위에서 고유 |
| aud | 대상 서비스 | 내 서비스가 대상에 포함되어야 함 |
| exp | 만료 시각 | 현재 시각이 exp 이상이면 거부 (RFC 7519: 현재 시각이 exp보다 이전이어야 처리 가능) |
| nbf | 이 시각 전에는 무효 | 현재 시각이 nbf 이전이면 거부 |
| iat | 발급 시각 | 토큰 나이 계산용 |
| jti | 토큰 고유 ID | 재사용·폐기 목록 관리에 사용. UUID를 흔히 씀 |
서버 간 시계가 몇 초 어긋날 수 있어 실무에서는 검증 시 수십 초 정도의 여유(leeway)를 두기도 합니다. 이 도구의 만료 표시는 열어 둔 브라우저 시계를 기준으로 단순 비교하며, 여유 시간은 반영하지 않습니다.
검산 예시
예시 1 (만료 계산): 데모 토큰의 iat는 1700000000, exp는 1700003600입니다. 두 값의 차이는 3600초 = 1시간이고, 1700000000은 2023-11-14 22:13:20 UTC입니다. 따라서 이 토큰은 1시간 수명이었고 지금은 이미 만료되었습니다. 만료 상태에서도 서명은 올바르므로 "서명 유효 + 만료됨"이 동시에 나올 수 있다는 점이 디코딩·검증·만료 판단이 서로 다른 단계임을 보여 줍니다.
예시 2 (밀리초 오류): 어떤 토큰의 exp가 1700003600000이라면 초로 해석할 때 서기 55,000년대의 먼 미래가 됩니다. 이는 사실상 만료 없는 토큰이므로 값을 1000으로 나눈 초 단위로 바로잡아야 합니다.
이 도구를 쓸 때의 한계
- RS256·ES256 등 공개키 서명 검증은 지원하지 않습니다. 공개키를 가진 서버/라이브러리에서 검증하세요.
- 암호화된 JWT(JWE)는 점이 5개이며 내용을 읽을 수 없습니다. 이 경우 안내 메시지가 표시됩니다.
- 이 도구의 "점검 결과"는 정적 규칙 기반의 힌트이며, 보안 감사를 대체하지 않습니다.
- JWT 안의 JSON 정리가 필요하면 JSON 포맷터를 함께 쓰세요.
자주 묻는 질문
입력한 JWT가 서버로 전송되나요?
아닙니다. 모든 디코딩과 서명 계산은 브라우저 안에서만 이뤄지며, 입력한 토큰 값은 어디로도 전송되지 않습니다. 다만 실제 서비스에서 발급된 토큰이라면 화면 공유나 캡처 시 노출되지 않도록 주의하는 것이 좋습니다.
왜 3부분(점 2개)으로 나뉘어 있나요?
JWT는 헤더.페이로드.서명 순서로 점(.)을 구분자로 이어붙인 구조입니다. 헤더에는 알고리즘 정보, 페이로드에는 실제 담고 있는 데이터(클레임), 서명에는 헤더와 페이로드가 변조되지 않았음을 증명하는 값이 들어 있습니다.
디코딩되면 토큰이 유효한 건가요?
아닙니다. 디코딩은 누구나 할 수 있는 읽기 동작일 뿐입니다. 유효성은 서명, 만료(exp), 발급자(iss), 대상(aud)을 서버가 직접 검증해야 확정됩니다.
서명 부분이 비어 있는 토큰은 무엇인가요?
alg가 none인 Unsecured JWT이며 끝이 점(.)으로 끝납니다. 일반적인 인증에는 쓰면 안 되고, 서버는 이를 거부하도록 설정해야 합니다.
페이로드를 수정하면 어떻게 되나요?
페이로드 문자가 한 글자라도 바뀌면 "헤더.페이로드" 문자열이 달라지므로 기존 서명으로는 검증에 실패합니다. 비밀키(또는 개인키)가 없으면 올바른 서명을 새로 만들 수 없습니다. 이 구조가 JWT가 변조를 막는 방식입니다.
한글이 깨지지 않고 나오는 이유는?
JWT의 JSON은 UTF-8 바이트를 Base64URL로 변환한 것입니다. 이 도구는 atob 결과의 바이트를 UTF-8로 해석하므로 한글 클레임도 정상 표시됩니다. 일부 구형 디코더는 이 과정을 생략해 한글이 깨집니다.