JWT 파서 가이드: JSON Web Token 안전하게 디코딩, 검증, 검사하기
프로덕션에서 인증이 갑자기 망가졌다. 사용자는 “Invalid Token” 오류를 겪고 있고, 빨리 원인을 찾아야 한다. JWT를 열어보면 알 수 없는 글자들뿐이다. 점으로 구분된 세 블록의 무작위 문자열. 데이터는 안에 들어 있지만, 파서 없이는 읽을 수 없다.
JWT Parser는 RFC 7519 표준에 따라 JSON Web Token의 세 부분 — Header, Payload, Signature — 를 분해하는 전용 도구다. 2026년 4월 현재, 이 파서들은 Base64URL로 인코딩된 데이터를 디코딩하고 비밀키 또는 공개키로 서명을 검증하여 토큰이 변조되지 않았음을 확인하며, “alg: none” 공격 같은 위협을 차단한다.
JWT 파서가 실제로 하는 일
JWT 파서를 번역기라고 생각해 보자. 길고 읽을 수 없는 문자열을 받아 다시 읽을 수 있는 JSON 객체로 되돌린다. 이는 현대 애플리케이션에서 사용자 신원을 관리하고 데이터 교환을 보호하는 데 필수적이다.
내부적으로 파서는 토큰을 세 구역으로 나누는 두 개의 마침표(.)를 찾는다.
| 구역 | 용도 | 인코딩 | 키 없이 읽기 가능? |
|---|---|---|---|
| Header | 메타데이터: 서명 알고리즘(HS256, RS256) | Base64URL | 예 |
| Payload | 클레임: 사용자 데이터, 만료, 역할 | Base64URL | 예 |
| Signature | 진본성을 증명하는 디지털 인봉 | HMAC/RSA | 아니오 — 키 필요 |

단계별 디코딩: 내부에서 일어나는 일
실제 토큰으로 따라가 보자. 이 예시 JWT를 보자.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE3MDAwMDAwMDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1단계: 마침표로 분할
[0] eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
[1] eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE3MDAwMDAwMDB9
[2] SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
2단계: 구역 [0]을 Base64URL 디코딩 (Header)
{
"alg": "HS256",
"typ": "JWT"
}
3단계: 구역 [1]을 Base64URL 디코딩 (Payload)
{
"sub": "1234567890",
"name": "John",
"iat": 1700000000
}
4단계: 구역 [2] 검증 (Signature) — 비밀키 필요
파서는 Base64URL 인코딩된 header + “.” + payload를 가져와 비밀키로 HMAC-SHA256을 계산한다. 결과가 구역 [2]와 일치하면 토큰은 진본이다.
중요 보안 참고: Base64URL은 암호화가 아니다
초보 개발자가 흔히 빠지는 함정이 인코딩된 header와 payload가 암호화되어 있다고 가정하는 것이다. 그렇지 않다. JustUse.me가 지적하듯, Base64URL 인코딩은 JSON을 URL과 헤더로 안전하게 전송할 수 있게 만들 뿐이다. 토큰을 가진 사람은 비밀번호나 키 없이도 payload를 디코딩할 수 있다.
절대 민감한 데이터(비밀번호, SSN, API 키)를 JWT payload에 저장하지 마라. 토큰을 가로채는 사람 누구에게나 보인다.
서명 검증: 보안 관문
누구나 토큰의 데이터를 읽을 수 있지만, 시스템을 실제로 안전하게 지키는 것은 서명 검증이다. JWT 파서는 단순히 정보를 읽는 게 아니라 — 그 출처를 증명한다.
파서는 header, payload, 그리고 키를 사용해 서명을 다시 계산한 뒤, 결과가 토큰의 서명과 일치하는지 확인한다. 일치하지 않으면 토큰이 변조된 것이다.
두 알고리즘 계열
| 알고리즘 | 키 유형 | 작동 방식 | 일반적 사용 사례 |
|---|---|---|---|
| HS256 (HMAC) | 대칭 — 서명과 검증에 동일한 비밀키 사용 | 양측이 하나의 비밀키를 공유 | 단일 서비스 인증, 한 팀 내 마이크로서비스 |
| RS256 (RSA) | 비대칭 — 개인키로 서명, 공개키로 검증 | 발신자가 개인키 보관; 공개키가 있는 누구나 검증 가능 | OAuth2 제공자, 서드파티 API 연동 |
| ES256 (ECDSA) | 비대칭 — RSA와 동일 모델이지만 타원 곡선 사용 | 더 작은 키, 더 빠른 검증 | 모바일 앱, 성능에 민감한 서비스 |

“alg: none” 공격
이것은 가장 위험한 JWT 취약점 중 하나다. 공격자가 header를 수정해 "alg": "none"이라고 주장하고 서명을 제거한다. 잘못 구현된 파서는 이를 받아들여, 검증 없이 토큰을 유효한 것으로 처리할 수 있다.
방어: 파서는 알고리즘이 “none”이거나 예상 알고리즘과 일치하지 않는 모든 토큰을 명시적으로 거부해야 한다. Apify JWT 도구를 개발한 Stas Persiianenko는 토큰이 본래 투명하게 설계되었지만, 그 보안은 파서가 서명되지 않거나 변조된 토큰을 엄격히 거부하는 데 달려 있다고 강조한다.
decoded = jwt.decode(token, key, algorithms=None) # NEVER do this
decoded = jwt.decode(token, key, algorithms=["HS256"])
표준 JWT 클레임: 각 필드의 의미
JWT 파서는 payload에서 “클레임”을 추출한다. 이는 시스템 간 호환성을 위해 JOSE (JSON Object Signing and Encryption) 프레임워크를 따른다.
| 클레임 | 전체 이름 | 용도 | 예시 값 |
|---|---|---|---|
iss |
Issuer | 토큰을 발급한 주체 | "auth.example.com" |
sub |
Subject | 토큰이 나타내는 사용자 또는 엔티티 | "user:12345" |
aud |
Audience | 토큰의 의도된 수신자 | "api.example.com" |
exp |
Expiration Time | 토큰이 무효가 되는 시점 | 1700000000 (Unix 타임스탬프) |
iat |
Issued At | 토큰이 생성된 시점 | 1699999999 |
nbf |
Not Before | 이 시점 이전에는 토큰이 유효하지 않음 | 1699999999 |
jti |
JWT ID | 토큰의 고유 식별자 | "a1b2c3d4" |
비대칭 서명을 사용할 때 파서는 종종 공개키를 나타내는 JSON 구조인 JWK (JSON Web Key) 를 참조한다. 파서는 발급자의 메타데이터 엔드포인트에서 올바른 JWK를 자동으로 가져와 토큰을 검증한다.
구현: 프로덕션용 실제 코드
PHP와 lcobucci/jwt
PHP 생태계의 표준은 lcobucci/jwt이다. Packagist 데이터에 따르면 2026년 4월 기준 322 million 회 이상 설치되었으며, Laravel과 Symfony 프로젝트의 기본 선택이다.
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
$config = Configuration::forSymmetricSigner(
new Sha256(),
InMemory::plainText('your-secret-key')
);
// Parsing and validating a token
$token = $config->parser()->parse($jwtString);
// Verify constraints: expiration, issuer, etc.
$constraints = [
new \Lcobucci\JWT\Validation\Constraint\IssuedBy('auth.example.com'),
new \Lcobucci\JWT\Validation\Constraint\PermittedFor('api.example.com'),
new \Lcobucci\JWT\Validation\Constraint\SignedWith(
$config->signer(),
$config->signingKey()
),
];
$isValid = $config->validator()->validate($token, ...$constraints);
Hono (Edge/Serverless)와 Web Crypto
가벼운 엣지 애플리케이션을 위해 Hono JWT Helper는 빠른 콜드 스타트와 최소 의존성이 필요한 서버리스 플랫폼에 딱 맞는 최소한의 decode() 함수를 제공한다.
import { jwt } from 'hono/jwt'
// Middleware to verify JWT on every request
app.use('/api/*', jwt({ secret: 'your-secret' }))
// Access decoded claims in your handler
app.get('/api/profile', (c) => {
const payload = c.get('jwtPayload')
return c.json({ user: payload.sub })
})
MCP로 AI 기반 JWT 분석
2026년이 되면서 Model Context Protocol (MCP) 이 Claude Code나 Cursor 같은 AI 어시스턴트가 JWT 도구와 직접 대화할 수 있게 해준다. MCP 서버를 설정하면 개발자는 AI에게 “이 로그의 모든 JWT에서 만료 오류를 확인해 줘”라고 요청할 수 있고 — 에이전트가 명령줄을 통해 파싱을 처리한다.
Apify에 따르면, 2026년 기준 대량 처리는 10,000 토큰당 약 $11.50의 비용이 든다. 이 자동화를 통해 AI 에이전트는 만료된 토큰을 찾고 앱의 보안 설정에 대한 코드 수정 사항을 즉시 제안할 수 있다.
결론
JWT 파서는 단순한 디버깅 편의 도구 그 이상이다 — 중요한 보안 검문소다. 서명 검사를 통해 토큰이 진본임을 보장하고, 클레임 검증을 통해 유효함을 보장한다. 가장 중요한 두 가지 규칙을 기억하라: Base64URL은 암호화가 아니므로 payload에 비밀을 넣지 마라. 그리고 “alg: none” 공격을 막기 위해 항상 허용 알고리즘을 명시적으로 지정하라.
프로덕션 앱에서는 직접 파서를 만들지 말고 lcobucci/jwt나 Hono의 JWT 헬퍼 같은 검증된 라이브러리를 사용하라. 디버깅과 대량 분석에는 AI 기반 MCP 도구가 보안 감사를 자동화하고 철저하게 유지하는 현대적 접근법이다.
FAQ
내 브라우저에서 발견한 JWT 토큰을 디코딩하는 것이 합법인가?
그렇다, 완전히 합법이다. JWT는 투명하게 설계되었다 — header와 payload는 비밀 유지를 위한 암호화가 아니라 전송을 위해 인코딩된 것이다. 토큰을 소유하고 있다는 것은 그 클레임의 데이터에 접근 권한이 있음을 의미한다. 단, 토큰에 개인정보가 포함된 경우 항상 GDPR 같은 현지 데이터 보호법을 준수해야 한다.
방금 생성한 토큰인데 JWT 파서가 isExpired: true를 표시하는 이유는?
이는 보통 토큰을 생성한 서버와 이를 파싱하는 시스템 간의 클럭 드리프트(clock drift) 로 인해 발생한다. 두 시스템의 시계가 (UTC/NTP를 통해) 동기화되어 있지 않으면 exp 또는 nbf 클레임이 무효로 보일 수 있다. 두 시스템이 시간 동기화에 NTP를 사용하도록 하거나, 파싱 라이브러리에 작은 “여유(leeway)”(보통 60 seconds)를 추가해 사소한 시간 어긋남을 흡수하도록 해 결결하라.
비밀키나 공개키 없이 JWT를 디코딩할 수 있나?
그렇다, header와 payload는 단순히 Base64URL로 인코딩된 JSON이기 때문에 키 없이도 언제나 디코딩하고 읽을 수 있다. 하지만 해당 비밀키(HS256의 경우) 또는 공개키(RS256의 경우) 없이는 서명을 검증하거나 데이터가 진본이라고 신뢰할 수 없다. 검증 없이는 해당 데이터를 미검증 상태이며 잠재적으로 변조되었을 수 있다고 취급하라.
“alg: none” 공격이란 무엇이며 어떻게 막나?
“alg: none” 공격은 토큰 header에 지정된 알고리즘을 검증 없이 받아들이는 파서를 악용한다. 공격자가 header를 "alg": "none"으로 변경하고 서명을 제거하여, 취약한 파저가 토큰을 유효한 것으로 받아들이도록 속인다. 검증 코드에서 항상 허용 알고리즘을 명시적으로 지정하여 이를 막아라 — 절대 “none”을 받아들이거나 토큰이 어떤 알고리즘을 사용할지 결정하도록 허용하지 마라.