JWT encode/decode
1. 디버깅을 위한 JWT 디코더
예전에는 세션 기반 인증이 대세였습니다. 서버가 로그인 상태를 직접 기억하고, 쿠키로 세션 ID만 주고받는 구조였죠. 그런데 서비스 규모가 커지고 마이크로서비스와 모바일 환경이 보편화되면서, 서버가 여러 대로 분산되자 세션을 어디에 저장하고 어떻게 공유할지 문제가 생겼기 때문에 JWT(JSON Web Token) 방식을 많이 사용하게 되었습니다. 왜냐하면 이런 환경에서 JWT는 서버가 상태를 저장하지 않아도 되는 stateless 특성과, 토큰 자체에 인증 정보가 모두 담겨 각 서버가 독립적으로 검증할 수 있는 특성이 있어 잘 맞기 때문입니다.
많이 사용하다 보니 요즘에는 회원관리 시스템이 있는 서비스에 대부분 JWT를 사용하는 것 같습니다. JWT는 Base64URL로 인코딩된 문자열이라, 토큰 안에 어떤 값이 담겨 있는지 눈으로 바로 확인하기가 어렵습니다. 물론 서버에서 디코딩 로직을 직접 돌려볼 수 있지만, 로그를 뒤지거나 코드를 잠깐 수정하는 과정이 번거롭게 느껴질 때가 많습니다. 그래서 온라인에서 즉시 확인할 수 있는 환경이 필요합니다..
그래서 만든 것이 이 온라인 JWT 디코더입니다. 토큰을 붙여넣기만 하면 헤더, 페이로드, 만료 시간을 즉시 확인할 수 있습니다. 새 프로젝트에서 JWT 발급 로직을 처음 짤 때 테스트용으로, 혹은 운영 중 401 오류가 발생했을 때 원인 파악용으로 빠르게 토큰을 디버깅하여 원하는 정보를 얻을 수 있습니다.
1. 도구 사용법 및 가이드
간단한 붙여넣기만으로 토큰의 내부 구조를 실시간으로 해독하고 유효성을 검증하는 방법을 안내합니다.

a. JWT 토큰 문자열 입력
분석하고자 하는 전체 JWT 문자열을 입력 창에 그대로 붙여넣습니다. 정상적인 토큰은 점(.)을 기준으로 헤더(Header), 페이로드(Payload), 서명(Signature) 세 부분으로 나뉘어 있어야 합니다. 입력과 동시에 별도의 버튼 클릭 없이 자동으로 디코딩 프로세스가 시작됩니다.
b. 실시간 구조 분석 및 파싱
입력된 데이터가 유효한 Base64URL 형식인지 시스템이 즉각적으로 판단합니다. 파싱이 완료되면 토큰의 메타데이터를 담고 있는 헤더, 실제 사용자 정보와 권한이 포함된 페이로드, 그리고 검증을 위한 서명 해시값이 화면 하단에 각각 깔끔한 JSON 포맷으로 분리되어 출력됩니다.
c. 만료 시간(exp) 및 유효성 확인
가장 번거로운 작업 중 하나인 토큰 만료 시간 확인을 도구가 자동으로 처리합니다. 페이로드 내에 'exp' 클레임이 존재할 경우, 해당 유닉스 타임스탬프를 한국 표준시(KST) 기준의 사람이 읽기 쉬운 날짜와 시간으로 변환해 줍니다. 현재 시각과 비교하여 토큰이 '유효'한 상태인지, 아니면 이미 '만료됨' 상태인지 직관적인 색상 라벨로 표시하여 디버깅 속도를 크게 높여줍니다.
2. JWT의 핵심 개념
JWT(JSON Web Token)는 RFC 7519로 정의된 개방형 표준으로, 2010년대 초반 SAML 토큰의 복잡함을 대체하기 위해 등장했습니다. XML 기반의 SAML에 비해 JSON 기반으로 가볍고, URL-safe한 문자열로 HTTP 헤더나 쿼리 파라미터에 자연스럽게 실어 보낼 수 있다는 장점이 빠르게 채택된 이유입니다.
a. 토큰의 세 가지 구성 요소
JWT는 점(.)으로 구분된 세 파트로 이루어져 있습니다.
xxxxx.yyyyy.zzzzz
↑ ↑ ↑
Header Payload Signature
| 구성 요소 | 역할 | 인코딩 방식 |
|---|---|---|
| Header | 토큰 타입(typ)과 서명 알고리즘(alg) 명시 | Base64URL |
| Payload | 사용자 클레임(Claim) 데이터 — 인증 정보의 본체 | Base64URL |
| Signature | Header + Payload를 비밀 키로 서명한 해시값 | 알고리즘에 따라 다름 |
Note
Header와 Payload는 암호화가 아니라 인코딩입니다. 누구나 디코딩하여 내용을 볼 수 있으므로, 비밀번호나 개인정보 같은 민감 데이터를 Payload에 담아서는 안 됩니다.
b. 주요 클레임(Claim) 정리
Payload에 들어가는 키-값 쌍을 **클레임(Claim)**이라고 부릅니다. JWT 표준은 자주 쓰이는 클레임에 대해 예약된 이름을 정의해 두었습니다.
| 클레임 | 이름 | 설명 | 예시 값 |
|---|---|---|---|
iss | Issuer | 토큰 발급자 | "https://auth.example.com" |
sub | Subject | 토큰의 대상(주로 사용자 ID) | "user-12345" |
aud | Audience | 토큰 수신자(API 서버 등) | "https://api.example.com" |
exp | Expiration Time | 만료 시각 (Unix timestamp, 초 단위) | 1717200000 |
nbf | Not Before | 이 시각 이전에는 토큰이 유효하지 않음 | 1717100000 |
iat | Issued At | 토큰 발급 시각 | 1717100000 |
jti | JWT ID | 토큰 고유 식별자 (중복 사용 방지) | "abc-123-def" |
Important
exp 값은 반드시 초(second) 단위 Unix timestamp이어야 합니다. JavaScript의 Date.now()는 밀리초를 반환하므로, 이를 그대로 넣으면 토큰이 발급 즉시 만료된 것으로 처리됩니다.
c. 서명 알고리즘 비교
// Header 예시
{
"alg": "HS256", // 서명에 사용할 알고리즘
"typ": "JWT"
}
| 알고리즘 | 방식 | 키 | 권장 용도 |
|---|---|---|---|
| HS256 | HMAC + SHA-256 | 대칭 키 (하나의 시크릿) | 단일 서버, 내부 서비스 간 통신 |
| RS256 | RSA + SHA-256 | 비대칭 키 (공개 키 / 개인 키) | 마이크로서비스, 외부 API 공개 |
| ES256 | ECDSA + SHA-256 | 비대칭 키 (타원 곡선) | 모바일, IoT 등 경량 환경 |
none | 서명 없음 | 없음 | 절대 프로덕션에서 사용 금지 |
3. 실무에서 JWT를 만나는 순간들
토큰 디코딩이 필요한 구체적인 실무 시나리오를 소개합니다.
a. API 디버깅 및 클라이언트 개발
프론트엔드 개발자가 백엔드 API로부터 로그인 성공 응답을 받았을 때, 발급된 토큰 안에 사용자 ID나 권한 롤(Role)이 올바르게 들어있는지 확인해야 합니다. 디코더를 사용하면 콘솔 창을 열거나 코드를 수정할 필요 없이 즉시 페이로드 데이터를 시각적으로 열람할 수 있습니다.
b. 타사 서비스 연동 데이터 검증
소셜 로그인(Google, Kakao, Apple 등)이나 외부 결제 시스템 등 서드파티 API와 연동할 때, 콜백으로 전달받은 토큰의 구조를 파악해야 하는 상황이 자주 발생합니다. iss, aud 등의 필수 클레임이 규격에 맞게 설정되었는지 빠르게 해독하여 연동 오류를 사전에 차단합니다.
c. 401 Unauthorized 원인 분석
서버가 401을 반환할 때, 원인이 토큰 만료인지 권한 부족인지 구분해야 합니다. 디코더로 exp를 즉시 확인하고, role이나 scope 클레임에 필요한 값이 포함되어 있는지 점검하면 서버 로그를 뒤지는 시간을 크게 줄일 수 있습니다.
d. 리프레시 토큰 플로우 검증
Access Token과 Refresh Token의 만료 시간 차이가 설계대로 설정되었는지 비교할 때 유용합니다. 두 토큰을 번갈아 붙여넣으며 exp 값을 대조하면 토큰 갱신 로직이 의도대로 동작하는지 빠르게 확인할 수 있습니다.
e. CI/CD 파이프라인 토큰 점검
GitHub Actions나 GitLab CI에서 사용하는 OIDC 토큰의 클레임을 확인해야 할 때가 있습니다. 배포 스크립트가 올바른 권한으로 클라우드 리소스에 접근하는지 디코딩하여 검증하는 데 활용됩니다.
4. 보안 고려사항
JWT는 편리하지만 잘못 사용하면 심각한 보안 취약점이 됩니다. 반드시 알아두어야 할 핵심 사항들을 정리합니다.
a. 디코딩 ≠ 검증
이 도구는 토큰의 내용을 읽는 것이지, 토큰이 위조되지 않았음을 증명하는 것이 아닙니다. 실제 무결성 검증은 반드시 서버에서 시크릿 키 또는 공개 키를 사용하여 수행해야 합니다.
// ❌ 클라이언트에서 토큰을 디코딩한 뒤 그 값을 신뢰
const payload = JSON.parse(atob(token.split('.')[1]));
if (payload.role === 'admin') {
showAdminPanel(); // 위조된 토큰이면 권한 탈취 가능
}
// ✅ 서버에서 서명 검증 후 신뢰할 수 있는 데이터만 응답
// 클라이언트는 서버 API 응답을 기반으로 권한 판단
const res = await fetch('/api/me', {
headers: { Authorization: `Bearer ${token}` }
});
const user = await res.json(); // 서버가 토큰을 검증한 결과
b. alg: "none" 공격 방어
JWT 헤더의 alg 필드를 "none"으로 변조하여 서명 검증을 우회하는 공격이 존재합니다. 서버 측에서는 반드시 허용할 알고리즘을 명시적으로 지정해야 합니다.
// ❌ 알고리즘을 토큰의 헤더에서 읽어서 사용
jwt.verify(token, secret); // alg: "none"이면 서명 없이 통과
// ✅ 허용할 알고리즘을 명시적으로 지정
jwt.verify(token, secret, { algorithms: ['HS256'] });
c. Payload에 민감 정보 금지
JWT Payload는 암호화되지 않습니다. 이 도구에서 토큰을 붙여넣기만 해도 내용이 보이는 것처럼, 누구나 디코딩할 수 있다는 점을 항상 기억하세요.
| 넣어도 되는 데이터 | 넣으면 안 되는 데이터 |
|---|---|
사용자 ID (sub) | 비밀번호 |
권한 롤 (role) | 신용카드 번호 |
토큰 만료 시간 (exp) | 주민등록번호 |
발급자 정보 (iss) | API 시크릿 키 |
Important
만약 Payload에 민감 데이터를 포함해야 하는 경우, JWT 대신 **JWE(JSON Web Encryption)**를 사용하여 Payload 자체를 암호화해야 합니다.
5. 트러블슈팅 — 분명 맞는 것 같은데 안 될 때
JWT를 다루다 보면 "분명히 올바른 토큰인데 왜 안 되지?" 하는 순간이 반드시 찾아옵니다. 가장 흔한 원인들을 정리합니다.
a. 토큰이 발급 즉시 만료 처리되는 경우
원인: exp 값에 밀리초 단위 타임스탬프를 넣었기 때문입니다. JWT 표준은 초 단위를 요구합니다.
// ❌ 밀리초 단위 — 서버가 이미 만료된 토큰으로 판단
const payload = {
sub: 'user-123',
exp: Date.now() + 3600000 // → 1717200000000 (13자리)
};
// ✅ 초 단위 — 올바른 만료 시간 설정
const payload = {
sub: 'user-123',
exp: Math.floor(Date.now() / 1000) + 3600 // → 1717200000 (10자리)
};
b. Base64 디코딩 시 깨진 문자가 출력되는 경우
원인: 일반 atob() 함수는 Base64URL이 아닌 표준 Base64를 처리합니다. JWT는 URL-safe 문자를 사용하므로 변환이 필요합니다.
// ❌ 표준 Base64 디코딩 — 특수문자가 포함되면 실패
const decoded = atob(token.split('.')[1]);
// ✅ Base64URL → Base64 변환 후 디코딩
function base64UrlDecode(str) {
str = str.replace(/-/g, '+').replace(/_/g, '/');
const pad = str.length % 4;
if (pad) str += '='.repeat(4 - pad);
return atob(str);
}
const decoded = base64UrlDecode(token.split('.')[1]);
c. 서버 간 시간 차이로 nbf 검증 실패
원인: 토큰을 발급하는 서버와 검증하는 서버의 시스템 시계가 수 초 이상 차이 나면, nbf(Not Before) 클레임 검증에서 실패할 수 있습니다.
// ✅ 검증 시 시계 오차 허용 (clock tolerance)
jwt.verify(token, secret, {
algorithms: ['HS256'],
clockTolerance: 30 // 30초까지 시간 차이 허용
});
d. 복사 과정에서 토큰이 잘리는 경우
원인: 슬랙, 이메일, 메신저 등에서 JWT를 공유할 때, 긴 문자열이 줄바꿈되거나 말줄임되어 토큰의 일부가 누락될 수 있습니다.
Note
토큰을 공유할 때는 코드 블록(```)으로 감싸거나, 텍스트 파일로 첨부하는 것이 안전합니다. 점(.)으로 구분된 세 파트가 모두 포함되어 있는지 확인하세요.
e. 공통 패턴: 대부분의 문제는 "표준 규격"을 놓친 것
위의 트러블슈팅 사례를 관통하는 공통점이 있습니다. JWT 표준(RFC 7519)이 정의한 규격 — 초 단위 타임스탬프, Base64URL 인코딩, 명시적 알고리즘 지정 — 을 정확히 따르지 않았을 때 문제가 발생한다는 점입니다. "대충 비슷하게" 구현하면 대부분의 경우 동작하지만, 엣지 케이스에서 반드시 깨집니다.
6. 생각해볼 거리
JWT의 stateless 특성은 정말 장점일까요?
JWT는 서버에 세션을 저장하지 않아도 된다는 점이 장점으로 소개됩니다. 하지만 이 stateless 특성 때문에 토큰을 발급한 뒤에는 서버가 이를 강제로 무효화할 수 없다는 근본적인 한계가 존재합니다. 사용자가 비밀번호를 변경하거나 계정이 탈취되어 즉시 로그아웃시켜야 할 때, 이미 발급된 JWT는 만료될 때까지 계속 유효합니다. 결국 실무에서는 블랙리스트 DB나 Redis를 두어 특정 토큰을 무효화하는 로직을 추가하게 되는데, 이 시점에서 JWT의 stateless 장점은 상당 부분 희석됩니다.
Access Token과 Refresh Token을 분리하는 이유는 무엇인가요?
Access Token의 수명을 짧게(보통 15분~1시간) 설정하면 탈취되더라도 피해 범위가 제한됩니다. 하지만 매번 로그인을 요구하면 사용자 경험이 나빠지므로, 수명이 긴 Refresh Token으로 새 Access Token을 자동 발급하는 구조를 사용합니다. Refresh Token은 서버 DB에 저장하고 관리할 수 있으므로, 필요시 개별 무효화가 가능합니다. 즉, "stateless의 편리함"과 "서버 제어의 안전함"을 절충한 설계입니다.
JWT를 localStorage에 저장하면 안 되나요?
localStorage는 JavaScript로 자유롭게 접근할 수 있기 때문에 XSS(Cross-Site Scripting) 공격에 취약합니다. 악성 스크립트가 주입되면 저장된 토큰을 탈취할 수 있습니다. 보안이 중요한 서비스에서는 토큰을 HttpOnly, Secure, SameSite 속성이 설정된 쿠키에 저장하는 것이 권장됩니다. 이 경우 JavaScript에서 토큰에 직접 접근할 수 없으므로 XSS를 통한 탈취가 원천적으로 차단됩니다. 다만 CSRF 공격에 대한 별도 대응이 필요하다는 점도 함께 고려해야 합니다.
브라우저 탭 하나로 인코딩된 JWT 문자열을 즉시 해독하고 만료 시간까지 확인할 수 있습니다 — 새벽 디버깅에서 터미널과 씨름하는 마찰이 사라집니다. 그것만으로도 이 도구의 역할은 충분하다고 생각합니다.