JWT 구조와 검증 — 디코딩과 서명 확인의 차이
예시 토큰을 헤더·페이로드·서명으로 분해해 각 부분의 생성 원리, 페이로드를 바꾸면 서명이 맞지 않는 이유, alg none과 알고리즘 혼동 공격, Base64와 Base64URL 차이, 서버가 토큰을 받아들이는 다섯 단계 검증 순서, HS256·RS256·ES256 선택 기준을 정리했습니다.
JWT를 JWT 디코더에 넣으면 헤더와 페이로드가 바로 읽힙니다. 그런데 이것은 디코딩이지 검증이 아닙니다. 페이로드의 사용자 ID를 바꿔 다시 인코딩한 토큰도 똑같이 잘 읽히고, 서명 부분을 통째로 지운 토큰도 읽힙니다. 두 토큰을 가르는 것은 서명 확인이고, 그 확인은 비밀키나 공개키를 가진 서버만 할 수 있습니다. 이 글은 하나의 예시 토큰을 세 부분으로 분해해 각 부분이 어떻게 만들어지는지, 페이로드를 한 글자 바꾸면 무슨 일이 생기는지, 서버가 토큰을 받아들이기까지 거쳐야 하는 검증 순서를 정리합니다.
예시 토큰 하나를 끝까지 따라가면
아래는 비밀키 my-secret-key로 HS256 서명한 247자짜리 토큰입니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iu2Zjeq4uOuPmSIsImlhdCI6MTc1NzExNjgwMCwiZXhwIjoxNzU3MTIwNDAwLCJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJhdWQiOiJzby15aS5jb20ifQ
.mOy7tDJarPiw7kbyfsDiqpWz_LMIpMHjHbSzLBuls_Y
| 부분 | 길이 | 디코딩하면 |
| -------- | ---- | ---------------------------------------------------------------------------------------------------------- |
| 헤더 | 36자 | {"alg":"HS256","typ":"JWT"} |
| 페이로드 | 166자 | {"sub":"1234567890","name":"홍길동","iat":1757116800,"exp":1757120400,"iss":"https://auth.example.com","aud":"so-yi.com"} |
| 서명 | 43자 | HMAC-SHA256(헤더.페이로드, 비밀키)의 256비트를 Base64URL로 적은 것 |
헤더와 페이로드는 JSON을 Base64URL로 인코딩한 것뿐이라 키 없이 누구나 되돌릴 수 있습니다. 서명은 앞 두 부분을 점으로 이은 문자열과 비밀키를 HMAC-SHA256에 넣은 32바이트를 인코딩한 것입니다. 32바이트를 Base64로 적으면 43자가 되므로 HS256 토큰의 서명은 항상 43자입니다. 페이로드의 iat 1757116800은 2025년 9월 6일 09:00(KST)이고 exp는 그로부터 3,600초 뒤라, 이 토큰은 발급 후 한 시간짜리입니다.
페이로드를 한 글자 바꾸면
같은 토큰에서 sub만 9999999999로 바꿔 다시 인코딩하면 페이로드 부분은 eyJzdWIiOiI5OTk5...로 시작하는 다른 문자열이 되고, 디코더에 넣으면 아무 문제 없이 읽힙니다. 하지만 서명은 원래 페이로드로 계산된 값이라 새 페이로드와 맞지 않습니다. 서버가 자기 비밀키로 헤더.새페이로드의 HMAC을 다시 계산하면 mOy7tDJa...가 아닌 전혀 다른 값이 나오고, 두 값이 다르므로 거부합니다.
비밀키가 다른 경우도 마찬가지입니다. 같은 헤더·페이로드를 other-key로 서명하면 MbXI7cElfUihPa70XN7rXj-MBYySzYL_bWXAvWmClhM가 나옵니다. 서명은 입력의 한 비트만 달라도 완전히 다른 값이 되므로, 공격자가 페이로드를 바꾸고도 통과하려면 비밀키 자체를 알아내야 합니다. HS256의 안전성은 전적으로 이 비밀키에 달려 있어, 짧은 문자열이나 예측 가능한 값을 쓰면 무차별 대입으로 뚫립니다. 최소 32바이트 이상의 난수를 쓰고, 소스 코드나 프런트엔드 번들에 절대 넣지 않아야 합니다.
서명이 아예 없는 토큰
헤더를 {"alg":"none","typ":"JWT"}로 바꾸고 서명 자리를 비우면 eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIi....처럼 끝이 점으로 끝나는 토큰이 됩니다. 이것도 디코더에서는 정상적으로 읽힙니다. 과거 일부 라이브러리는 헤더의 alg를 그대로 믿고 none이면 서명 검증을 건너뛰었기 때문에, 공격자가 관리자 ID를 넣은 무서명 토큰으로 로그인할 수 있었습니다.
같은 원리의 변종으로 알고리즘 혼동 공격이 있습니다. 서버가 RS256 공개키로 검증하도록 되어 있는데 공격자가 헤더를 HS256으로 바꾸고 그 공개키를 HMAC 비밀키로 써서 서명하면, alg를 믿는 라이브러리는 공개키로 HMAC을 검증해 통과시킵니다. 공개키는 이름 그대로 공개된 값이라 누구나 유효한 토큰을 만들 수 있게 됩니다. 두 공격 모두 서버가 허용할 알고리즘을 코드에 고정하면 막힙니다. 토큰이 무엇을 주장하든 서버는 자신이 정한 알고리즘과 키로만 검증해야 합니다.
Base64와 Base64URL은 다르다
JWT는 일반 Base64가 아니라 Base64URL을 씁니다. 같은 JSON {"name":"홍길동","role":"admin?>"}을 두 방식으로 인코딩하면 이렇게 갈립니다.
| 방식 | 결과 |
| ---------- | -------------------------------------------------------- |
| Base64 | eyJuYW1lIjoi7ZmN6ri464+ZIiwicm9sZSI6ImFkbWluPz4ifQ== |
| Base64URL | eyJuYW1lIjoi7ZmN6ri464-ZIiwicm9sZSI6ImFkbWluPz4ifQ |
+가 -로, /가 _로 바뀌고 끝의 = 패딩이 사라집니다. URL이나 쿠키에 그대로 넣어도 깨지지 않게 하려는 것입니다. 일반 Base64 함수로 JWT 부분을 디코딩하면 -와 _에서 실패하거나 패딩이 없다고 오류가 나므로, 직접 디코딩할 때는 두 문자를 되돌리고 길이가 4의 배수가 되도록 =를 채워야 합니다. Base64 변환기의 URL-safe 옵션이 이 변환을 해 줍니다. 한글처럼 여러 바이트인 문자는 UTF-8 바이트 열을 인코딩하므로 페이로드에 한글 이름이 있어도 문제없이 오가지만, 그만큼 토큰이 길어집니다.
서버가 토큰을 받아들이는 순서
검증은 서명 하나로 끝나지 않습니다. 순서대로 확인해야 할 것이 다섯 가지입니다.
- 형식: 점으로 나뉜 세 부분인지, 각 부분이 Base64URL로 디코딩되고 헤더·페이로드가 JSON 객체인지 봅니다. 여기까지가 디코더가 하는 일입니다.
- 알고리즘 고정: 헤더의
alg를 읽어 판단하지 말고, 서버 설정에 적힌 알고리즘(예: RS256만)과 다르면 즉시 거부합니다. - 서명: HS256이면 비밀키로 HMAC을 재계산해 비교하고, RS256·ES256이면
kid에 맞는 공개키로 서명을 검증합니다. 비교는 시간이 일정한 함수를 써야 타이밍 공격을 피합니다. - 시간 클레임:
exp가 지났는지,nbf가 아직 안 왔는지 봅니다. 서버 간 시계 차이를 감안해 30초에서 1분 정도의 여유를 두는 것이 관례이고, 밀리초를 초로 착각해 넣은exp는 5만 년 뒤 날짜가 되어 영원히 만료되지 않으니 발급 코드에서 단위를 확인해야 합니다. - 발급자와 대상:
iss가 신뢰하는 발급자인지,aud에 이 서비스가 들어 있는지 확인합니다. 같은 발급자가 여러 서비스에 토큰을 내줄 때 다른 서비스용 토큰을 받아들이는 사고를 막는 것이aud입니다.
이 다섯 단계를 통과해야 페이로드의 sub를 믿고 사용자를 식별할 수 있습니다. 라이브러리의 verify 함수는 대개 이 순서를 한 번에 처리하지만, 허용 알고리즘과 iss·aud 값은 옵션으로 넘겨야 검사되므로 기본값에 맡기면 4단계까지만 확인하고 끝나는 경우가 많습니다.
HS256과 RS256 중 무엇을 쓰나
같은 페이로드를 RS256(2048비트 키)으로 서명하면 서명이 342자로 늘어 토큰 전체가 576자가 됩니다. HS256의 247자보다 두 배 이상 길지만, 검증하는 쪽이 공개키만 가지면 되므로 발급 서버와 검증 서버가 다를 때 비밀키를 나눠 갖지 않아도 됩니다. 소셜 로그인의 ID 토큰, 여러 마이크로서비스가 공유하는 액세스 토큰이 RS256이나 ES256을 쓰는 이유입니다. 공개키는 발급자의 JWKS 주소에서 받아 kid로 골라 쓰고, 키가 바뀌면 새 kid가 등장하므로 서버는 JWKS를 주기적으로 다시 받아야 합니다. ES256은 서명이 86자로 RS256보다 훨씬 짧으면서 같은 수준의 안전성을 주어 새로 설계한다면 우선 고려할 만합니다. 한 서비스 안에서만 발급하고 검증한다면 HS256이 간단하고 빠릅니다.
함께 확인하면 좋은 것
토큰의 헤더·페이로드와 만료 여부를 확인하려면 JWT 디코더를, 각 부분을 직접 인코딩·디코딩해 보려면 Base64 변환기를, exp와 iat의 초 단위 값을 날짜로 바꾸려면 타임스탬프 변환기를 쓰세요.
이 글에서 다룬 도구
더 읽을 글
- 2026년 4대보험 요율 총정리
국민연금 9.5%, 건강보험 7.19%, 장기요양 13.14%, 고용보험 1.8%. 근로자·사업주 부담과 월 보수별 보험료, 국민연금 인상 로드맵을 정리했습니다.
- 2026년 연봉 실수령액표
연봉 2,000만 원부터 1억 원까지 500만 원 단위로 월 실수령액과 공제율을 정리했습니다. 부양가족·비과세·퇴직금 포함 여부에 따른 차이도 함께 봅니다.
- 2026년 최저임금 월급·주휴수당 계산법
시급 10,320원, 월 2,156,880원. 209시간의 정체, 주 근로시간별 알바 주급·월급, 야간·연장 가산, 수습 감액 조건과 위반 대응을 정리했습니다.
- 퇴직금 지급 조건과 평균임금 계산 방법
1년 이상·주 15시간 이상이면 아르바이트도 받습니다. 평균임금에 들어가는 것과 빠지는 것, 상여금 3/12, 통상임금 보장, DB·DC형 차이, 지급 기한을 정리했습니다.