본문 바로가기
Converter

JWT 디코더 (토큰 내용 확인)

header·payload·서명을 나눠서 보고, 만료 시각을 확인

토큰은 브라우저 안에서만 디코딩됩니다. 서버로 전송되지 않습니다.

header.payload.signature — Bearer 접두어나 따옴표가 붙어 있어도 됩니다

JWT의 내용은 잠긴 게 아니라 봉인된 것입니다

JWT를 처음 다루는 사람이 가장 자주 오해하는 지점부터 짚겠습니다. 토큰이 길고 알파벳과 숫자가 뒤섞인 암호처럼 생겨서 “내용이 감춰져 있다”고 느끼기 쉽지만, 그렇지 않습니다. JWT의 헤더와 페이로드는 암호화가 아니라 인코딩일 뿐입니다. 위쪽 변환기에 토큰을 붙여넣으면 사용자 ID도, 권한도, 만료 시각도 즉시 읽힙니다. 키도 비밀도 필요 없습니다.

비유하자면 잠긴 상자가 아니라 봉인된 편지입니다. 봉인(서명)은 누가 중간에 뜯어고쳤는지를 드러낼 뿐, 편지를 읽는 것 자체는 막지 못합니다. 서명이 보장하는 것은 위조 방지이지 기밀이 아닙니다. 이 한 문장이 JWT를 다루는 모든 판단의 출발점입니다. 그래서 페이로드에 무엇을 넣어도 되는지, 왜 이 도구가 서명을 검증하지 않는지가 전부 여기서 따라 나옵니다.

세 조각: 헤더 · 페이로드 · 서명

JWT(정확히는 서명 방식인 JWS)는 점(.) 두 개로 나뉜 세 조각입니다.

eyJhbGciOiJIUzI1NiJ9  .  eyJzdWIiOiIxMjMifQ  .  dBjftJeZ4CVP...
    헤더(Header)          페이로드(Payload)        서명(Signature)

세 조각 모두 base64url로 인코딩되어 있습니다. 일반 Base64가 아니라 URL-safe 변형인 이유는 이 토큰이 URL과 HTTP 헤더에 실려 다니기 때문입니다 — 표준 Base64의 +, /, =는 URL에서 사고를 칩니다. 그 사정은 Base64 인코더 해설에 자세히 적어 두었습니다. 여기서 알아 둘 것은 하나입니다: 조각을 잇는 구분자로 을 쓰는 이유는, 점이 base64url 알파벳에 포함되지 않기 때문입니다. 데이터 안에 절대 나타나지 않으니 경계로 안전합니다.

헤더는 “이 토큰을 어떻게 검증하는가”를 말한다

헤더는 대개 두 줄짜리 JSON입니다.

{ "alg": "HS256", "typ": "JWT" }

typ은 이 토큰의 종류(JWT), alg는 서명에 쓴 알고리즘입니다. 별것 아니어 보이는 alg가 사실 JWT 보안에서 가장 사고가 많이 난 필드입니다. 이유는 다음 절에 있습니다. 이 밖에 kid(어떤 키로 서명했는지 가리키는 키 ID)가 자주 보이는데, 검증하는 서버가 여러 개의 키를 돌려 쓸 때 “이 토큰은 그중 이 키로 검증하라”는 표지 역할을 합니다.

가장 위험한 함정: 헤더의 alg를 믿지 마세요

직관적으로는 “토큰이 자기가 HS256으로 서명됐다고 하니 HS256으로 검증하면 되겠다” 싶습니다. 바로 그 직관이 JWT 역사상 가장 유명한 취약점을 만들었습니다. 문제는 공격자가 헤더를 마음대로 고칠 수 있다는 데 있습니다. 헤더는 그냥 base64url 이니까요.

alg: none 공격

JWT 명세에는 “서명 없음”을 뜻하는 alg: none이 있습니다. 원래는 이미 다른 계층에서 보호되는 토큰을 위한 것이었는데, 초기의 여러 검증 라이브러리가 헤더의 alg그대로 믿고 분기하는 바람에 사고가 났습니다. 공격자가 페이로드를 { "role": "admin" }으로 바꾸고, 헤더의 alg none으로 바꾸고, 서명 조각을 비워서 보내면 — 라이브러리가 “none이라니 서명 검증은 건너뛰자”며 통과시켜 버린 것입니다. 위 변환기가 alg: none토큰에 경고를 띄우는 이유가 이것입니다.

RS256을 HS256으로 바꿔치기하는 공격

조금 더 교묘한 변형도 있습니다. 서버가 RS256(공개키로 검증)을 쓴다고 합시다. 이때 공개키는 말 그대로 공개되어 있습니다. 공격자가 헤더의 alg HS256으로 바꾸고, 그 공개키 문자열을 HMAC의 비밀키로 삼아 서명을 만들어 보냅니다. 검증하는 서버가 alg를 헤더에서 읽어 “HS256이네” 하고는 자기가 가진 공개키로 HMAC 검증을 하면 — 서명이 맞아떨어집니다. 공개키는 누구나 알 수 있으니 위조가 성립합니다.

두 공격의 교훈은 같습니다. 검증하는 쪽은 알고리즘을 토큰에서 읽으면 안 됩니다. 서버가 “이 엔드포인트는 RS256만 받는다”고 고정해 두고, 헤더의 alg가 그와 다르면 그 자리에서 거절해야 합니다. 이 도구가 헤더를 보여 주는 것도 같은 맥락입니다 — 토큰이 스스로 뭐라고 주장하는지 눈으로 확인하되, 그 주장을 신뢰의 근거로 삼지 않는 감각을 기르는 것입니다.

페이로드: 등록된 7개의 클레임

페이로드에 담기는 각 항목을 클레임(claim)이라고 부릅니다. 아무 이름이나 넣어도 되지만(비공개 클레임), 표준(RFC 7519)이 뜻을 정해 둔 등록 클레임 일곱 개는 알아 두면 대부분의 토큰이 읽힙니다. 위 변환기는 이 일곱 개를 만나면 자동으로 한국어 뜻을 붙여 보여 줍니다.

  • iss (issuer) — 누가 발급했나. 인증 서버의 주소나 이름.
  • sub (subject) — 이 토큰이 가리키는 주체. 보통 사용자 ID.
  • aud (audience) — 누구를 위한 토큰인가. 이 토큰을 받아들여도 되는 대상.
  • exp (expiration) — 만료 시각. 이 시각이 지나면 무효.
  • nbf (not before) — 이 시각 이전에는 유효하지 않음.
  • iat (issued at) — 발급된 시각.
  • jti (JWT ID) — 토큰의 고유 식별자. 재사용(replay) 차단에 쓴다.

여기서 exp, nbf, iat 세 개는 값이 숫자입니다 — 정확히는 1970년 1월 1일부터 흐른 초, 즉 Unix 타임스탬프(NumericDate)입니다. 그래서 1784354400 같은 숫자로 적혀 있고, 사람이 읽으려면 날짜로 바꿔야 합니다. 위 변환기가 이 세 클레임 아래에 KST 날짜를 함께 적어 주는 이유입니다. 이 숫자를 직접 다뤄 볼 일이 있으면 Unix 타임스탬프 변환기가 같은 값을 양방향으로 바꿔 줍니다.

exp는 누구의 시계로 만료되나

exp가 초 단위 UTC 시각이라는 점은 실무에서 은근히 자주 문제가 됩니다. 만료 판단은 최종적으로 서버 시계로 이뤄집니다. 이 페이지가 “만료됨” 이나 “55분 후 만료”라고 보여 주는 것은 당신 브라우저의 시계로 계산한 참고값일 뿐입니다. 브라우저 시계가 몇 분 틀어져 있으면 서버와 다르게 보일 수 있고, 그래서 이 도구는 “검증했다”가 아니라 “당신 시계 기준으로는 이렇다”고만 말합니다.

(기술적으로 덧붙이면, 이 페이지가 만료 뱃지를 페이지가 뜬 다음에계산하는 것도 같은 원리 때문입니다. 서버에서 미리 “만료됨”을 그려 두면 서버 시각으로 굳어 버려서, 당신이 실제로 보는 시각과 어긋납니다. “지금”은 당신 브라우저에서만 정직하게 알 수 있습니다.)

그래서 이 도구는 서명을 검증하지 않습니다

여기까지 오면 이 도구가 서명 검증을 안 하는 게 아니라 못 하는 게 아니라, 하면 안 되는 이유가 분명해집니다. 검증하려면 키가 필요합니다 — HS256이면 발급자의 비밀키, RS256이면 발급자의 공개키입니다. HS256의 비밀키를 이 웹페이지가 알고 있으려면 그 키가 브라우저로 내려와야 하는데, 그 순간 키는 더 이상 비밀이 아닙니다. 누구나 개발자 도구로 꺼내 볼 수 있으니까요.

디코딩과 검증은 다른 일입니다.디코딩은 base64url을 풀어 JSON을 읽는 것, 검증은 키로 서명을 다시 계산해 맞는지 대조하는 것입니다. 브라우저에서 안전하게 할 수 있는 건 앞의 것뿐입니다. 그래서 이 도구는 헤더·페이로드·만료 시각을 보여 주되, 서명 칸 옆에는 “검증하지 않음”이라고 분명히 적습니다. 안 한 일을 한 것처럼 “유효한 토큰”이라는 초록불을 켜는 것이 — 이 사이트가 가장 경계하는, 사용자를 오해하게 만드는 표시입니다.

페이로드에 넣지 말아야 할 것

맨 처음 이야기로 돌아옵니다. 페이로드는 누구나 읽을 수 있습니다. 그러므로:

  • 비밀·민감정보 금지.비밀번호, 주민등록번호, 카드번호는 물론이고 “이 정도는 괜찮겠지” 싶은 내부 식별자도 넣지 않는 편이 안전합니다. 봉인은 위조만 막지 열람을 막지 않습니다.
  • 토큰은 작게. JWT는 매 요청마다 HTTP 헤더에 실려 다닙니다. 페이로드에 사용자 프로필을 통째로 넣으면 모든 요청이 그만큼 무거워집니다. 필요한 최소한 (ID·권한·만료)만 담고 나머지는 서버에서 조회하는 편이 낫습니다.
  • 만료는 짧게. 유출됐을 때의 피해는 exp까지의 시간에 비례합니다. 짧게 두고 갱신(refresh) 토큰으로 연장하는 구조가 표준적인 이유입니다.

정리

JWT는 서명된, 그러나 공개된 JSON입니다. 세 조각(헤더·페이로드·서명)이 base64url로 이어져 있고, 헤더의 alg는 토큰의 주장일 뿐 신뢰의 근거가 아니며, exp는 서버 시계로 판단되는 초 단위 UTC 시각입니다. 이 도구는 그 내용을 눈으로 확인하게 해 줄 뿐, 서명 검증은 키를 쥔 서버의 몫으로 남겨 둡니다. 토큰을 설계할 때는 페이로드가 전부 읽힌다고 가정하고, 검증하는 코드를 짤 때는 알고리즘을 토큰이 아니라 서버가 정한다고 기억하면, JWT에서 사고가 나는 길목 대부분을 피할 수 있습니다.

자주 묻는 질문

JWT 토큰의 내용을 아무나 볼 수 있나요?
네, 볼 수 있습니다. JWT의 헤더와 페이로드는 암호화가 아니라 base64url로 인코딩되어 있을 뿐입니다. 토큰을 가진 사람은 누구나 이 페이지 같은 도구에 붙여넣어 사용자 ID, 권한, 만료 시각을 그대로 읽을 수 있습니다. 서명이 막아 주는 것은 '읽기'가 아니라 '위조'입니다. 그래서 JWT 페이로드에는 주민번호·비밀번호 같은 값을 넣으면 안 됩니다.
이 도구가 토큰의 서명도 검증하나요?
아니요. 이 도구는 디코딩만 합니다. 서명을 검증하려면 발급자의 비밀키(HS256) 또는 공개키(RS256)가 필요한데, 그 키는 브라우저에 두면 안 되는 값입니다. 디코딩과 검증은 다른 일이고, 여기서는 앞의 것만 정직하게 합니다. 만료 시각(exp)을 날짜로 보여 주긴 하지만, 그것도 '서버가 이렇게 판단할 것'이라는 참고일 뿐 검증 결과가 아닙니다.
exp가 아직 안 지났는데 서버에서는 왜 만료라고 하나요?
exp는 초 단위 UTC 시각이고, 최종 판단은 항상 서버 시계로 이뤄집니다. 이 페이지가 보여 주는 '만료됨/유효'는 당신 브라우저의 시계를 기준으로 계산한 값이라, 브라우저 시계가 몇 분 어긋나 있으면 서버와 다르게 보일 수 있습니다. 또 많은 서버가 시계 오차를 감안해 수십 초의 여유(clock skew)를 두기 때문에, 경계 근처에서는 판단이 갈릴 수 있습니다.
헤더의 alg가 none이면 무슨 뜻인가요?
서명 알고리즘이 '없음'이라는 뜻이고, 서명 조각이 비어 있는 토큰입니다. 서버가 이런 토큰을 그대로 받아들이면 누구나 페이로드를 고쳐서 넣을 수 있어 사실상 인증이 무력화됩니다. 과거 여러 JWT 라이브러리가 헤더에 적힌 alg를 그대로 믿고 검증하다가 이 공격에 뚫렸습니다. 검증 코드는 알고리즘을 헤더에서 읽지 말고 서버가 고정해 두어야 합니다.
여기에 실제 운영 토큰을 붙여넣어도 안전한가요?
이 페이지는 모든 디코딩을 당신의 브라우저 안에서 처리하고, 토큰을 서버로 보내지 않습니다(네트워크 요청이 없습니다). 다만 유효한 액세스 토큰은 그 자체가 로그인 세션이므로, 화면 공유 중이거나 공용 PC에서는 주의하세요. 원리를 익히는 용도라면 만료된 토큰이나 예시 토큰으로 연습하는 편이 안전합니다.
HS256과 RS256은 뭐가 다른가요?
서명에 쓰는 키가 다릅니다. HS256은 대칭키로, 하나의 비밀키로 서명도 하고 검증도 합니다 — 검증하는 쪽도 그 비밀키를 알아야 하므로, 발급자와 검증자가 같거나 서로를 신뢰하는 경우에 씁니다. RS256은 비대칭키로, 비밀키로 서명하고 공개키로 검증합니다 — 검증하는 여러 서비스에 공개키만 나눠 주면 되고 비밀키는 발급 서버만 갖습니다. 외부에 검증을 맡겨야 하면 RS256이 안전합니다.