JWT 토큰 검증, 서명 위변조 여부 확인하는 법

JWT 서명을 검증하면 토큰이 발급 당시 내용 그대로인지, 그리고 그 서명이 내가 아는 비밀키로 만들어진 게 맞는지 확인할 수 있습니다. API 응답으로 받은 토큰이 위변조되지 않았는지 직접 확인하고 싶은 개발자를 위한 글입니다.
서명이 어떻게 계산·비교되는지, 그리고 "검증 실패"가 뜨는 두 가지 서로 다른 원인을 코드 동작 그대로 설명합니다.
요약 ① JWT는 header.payload.signature 세 부분이 마침표로 이어진 문자열이며, header와 payload는 암호화가 아니라 Base64Url로 인코딩만 되어 있습니다. ② signature는 header와 payload를 합친 문자열을 비밀키로 HMAC-SHA256 계산한 값이며, 검증은 같은 계산을 다시 해서 값이 같은지 비교하는 것입니다. ③ 검증 실패는 비밀키가 다른 경우와 내용이 위변조된 경우 모두에서 똑같이 나타나며, 실패 메시지만으로는 어느 쪽인지 구분되지 않습니다.
검증 실패는 봤는데, 원인은 알 수 없는 상황
백엔드 API가 로그인 시 JWT를 발급하고, 이후 요청마다 클라이언트가 Authorization 헤더에 이 토큰을 담아 보내는 구조는 흔합니다. 서버는 요청이 올 때마다 토큰의 서명을 다시 계산해, 정말 우리 서버가 발급한 토큰이 맞는지 확인합니다.
문제는 이 검증에서 "실패"가 뜨는 순간입니다. 서버를 재배포하며 환경변수의 시크릿 키가 바뀌었거나, 테스트 환경에서 다른 키를 잘못 넣은 경우도 화면에는 위변조된 토큰과 똑같이 "실패"로만 나타납니다. 원리를 모르면 애먼 곳에서 원인을 찾게 됩니다.
또 하나 흔한 오해는 "payload에 든 사용자 정보는 암호화되어 있어 안전하다"는 생각입니다. 실제로 payload는 Base64Url로 인코딩만 된 것이라, 토큰 문자열만 있으면 누구나 그 내용을 읽을 수 있습니다.
이 오해 때문에 비밀번호나 주민등록번호 같은 민감정보를 payload에 그대로 넣는 실수가 생기기도 합니다.
도구 없이 서명을 직접 계산해 검증하는 방법
JWT는 마침표(.)로 이어진 세 부분입니다. header.payload.signature 순서이며, 각각 다음을 담습니다.
- header: 서명 알고리즘(alg)과 토큰 타입(typ)을 담은 JSON
- payload: 실제로 담고 싶은 데이터(claims)를 담은 JSON
- signature: header와 payload를 합친 문자열을 비밀키로 서명한 값
header와 payload는 각각 JSON을 Base64Url로 인코딩한 것입니다. 암호화가 아니라 인코딩이므로 별도의 키 없이 누구나 원래 JSON으로 되돌릴 수 있습니다.
예를 들어 HS256 토큰의 header 부분인 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9는 아래처럼 디코딩됩니다.
{"alg":"HS256","typ":"JWT"}
Base64Url은 표준 Base64와 문자 두 개가 다릅니다. +가 -로, /가 _로 바뀌어 있고, 끝의 = 패딩은 생략되어 있습니다. 표준 base64 디코더를 쓰려면 이 두 글자를 되돌리고 패딩을 채워 넣어야 합니다.
signature를 검증하는 절차는 다음과 같습니다. 먼저 토큰의 header 부분과 payload 부분을 마침표로 다시 이어 header.payload 문자열을 만듭니다.
이 문자열을 발급 때 쓴 것과 같은 비밀키로 HMAC-SHA256 계산한 뒤 Base64Url로 인코딩하면, 토큰의 세 번째 부분과 정확히 같아야 합니다.
printf '%s' "$HEADER.$PAYLOAD" | openssl dgst -sha256 -hmac "시크릿키" -binary | openssl base64
이 계산은 **결정적(deterministic)**입니다. 같은 문자열과 같은 키를 넣으면 언제나 같은 서명이 나옵니다.
header나 payload를 단 한 글자만 바꿔도 재계산한 서명은 완전히 다른 값이 되어 원래 서명과 더는 일치하지 않습니다. "위변조를 감지한다"는 말은 실제로는 이 비교 하나로 이루어집니다.
손으로 하면 Base64와 Base64Url의 문자 치환, 생략된 패딩 복원, HMAC 명령어 문법에서 실수하기 쉽습니다. 매번 이 과정을 반복하는 것도 번거롭습니다.
저희 도구로 확인하기.
브라우저에서 즉시 서명을 계산·비교합니다. 입력한 비밀키와 토큰은 서버로 전송되지 않습니다.

검증 실패의 두 가지 원인, 그리고 인코딩과 암호화 혼동
"검증 실패"라는 결과 하나에는 서로 다른 두 원인이 겹쳐 있습니다. 재계산한 서명이 토큰의 서명과 다르면 원인과 무관하게 똑같이 "실패"로만 표시되기 때문입니다.
| 원인 | 무엇이 다른가 | 확인하는 방법 |
|---|---|---|
| 시크릿 키 불일치 | 서명 계산에 쓴 키 자체가 발급 당시와 다름 | 발급 서버가 쓰는 키와 완전히 같은 문자열인지(공백·줄바꿈 포함) 다시 대조 |
| 내용 위변조 | header 또는 payload의 JSON 값이 발급 이후 바뀜 | 발급 때와 같은 키를 넣었는데도 실패하면 내용이 바뀐 쪽 |
즉 검증 실패 메시지 자체는 두 원인을 구분해주지 않습니다. 올바른 키를 확실히 알고 있는 상태에서 실패가 난다면, 그때 비로소 "내용이 바뀌었다"고 좁혀볼 수 있습니다.
두 번째 혼동은 "payload는 암호화되어 있다"는 생각입니다. 인코딩과 암호화는 목적 자체가 다릅니다.
| 항목 | Base64Url 인코딩 (JWT payload) | 암호화 |
|---|---|---|
| 목적 | 데이터를 텍스트로 옮겨 적기 | 내용을 제3자가 읽지 못하게 숨기기 |
| 복원에 키가 필요한가 | 아니요 | 예 |
| 원본 복원 가능 범위 | 토큰 문자열만 있으면 누구나 | 올바른 복호화 키를 가진 사람만 |
서명(signature)이 보장하는 것은 무결성(발급 이후 바뀌지 않았음)과 발급자 확인(그 비밀키를 아는 쪽이 만들었음)입니다. 기밀성(내용을 숨기는 것)은 보장하지 않습니다.
payload에 담긴 값은 서명이 유효한지와 무관하게 토큰을 가진 누구나 디코딩해 읽을 수 있습니다.
정리
- JWT는 header.payload.signature 세 부분이 마침표로 이어진 문자열입니다.
- header와 payload는 Base64Url 인코딩일 뿐이라 누구나 디코딩해 읽을 수 있습니다. 암호화가 아닙니다.
- signature는 header.payload 문자열을 비밀키로 HMAC-SHA256 계산한 값이며, 검증은 이 계산을 다시 해서 값이 같은지 비교하는 것입니다.
- "검증 실패"는 시크릿 키 불일치와 내용 위변조 두 경우 모두에서 똑같이 나타나며, 메시지만으로는 구분되지 않습니다.
- 서명은 무결성과 발급자 확인용이지, payload 내용을 숨기는 기능이 아닙니다.
브라우저에서 즉시 서명을 계산·비교합니다. 입력한 비밀키와 토큰은 서버로 전송되지 않습니다.
자주 묻는 질문
JWT의 payload를 암호화 없이 그냥 봐도 되나요?
디코딩만 하면 내용을 볼 수 있습니다. header와 payload는 Base64Url로 인코딩된 것이라, 별도의 키 없이 누구나 원래 JSON으로 되돌릴 수 있습니다. 이 도구의 검증 탭도 서명이 맞는지와 무관하게 항상 디코딩된 JSON을 함께 보여줍니다.
검증이 실패하면 무조건 토큰이 위조된 건가요?
아닙니다. 검증에 사용한 시크릿 키가 발급 당시 키와 다른 경우에도 똑같이 실패로 나타납니다. 발급 서버가 쓰는 키와 정확히 같은 문자열을 넣었는데도 실패한다면, 그때 내용이 바뀌었을 가능성을 살펴볼 차례입니다.
RSA 같은 비대칭키 알고리즘으로 서명된 토큰도 검증할 수 있나요?
이 도구는 HMAC-SHA256(HS256) 대칭키 서명만 지원합니다. RSA 등 비대칭키 알고리즘(RS256 등)으로 서명된 토큰은 이 도구로 검증할 수 없습니다.
이 도구에 입력한 시크릿 키나 토큰이 서버로 전송되나요?
전송되지 않습니다. 서명 생성과 검증은 브라우저의 Web Crypto API를 이용해 기기 안에서만 계산되며, 입력한 값은 외부로 나가지 않습니다.
