JWT란 무엇이고 어떻게 읽나
API를 붙이다 보면 eyJhbGciOi...로 시작하는 긴 문자열을 보게 돼요. 로그인 응답이나 요청 헤더에 들어 있는 이 문자열이 JWT(JSON Web Token)예요. 안에 무엇이 들어 있는지 직접 읽는 방법을 알아 두면 "토큰이 왜 만료됐지?", "이 서버에 왜 401이 나지?" 같은 문제를 훨씬 빨리 풀 수 있어요. 이 글은 예시 토큰을 한 조각씩 풀어 보면서 구조와 표준 필드, 그리고 자주 하는 실수를 정리해요.
1. JWT의 세 부분
JWT는 점(.)으로 이어진 세 덩어리예요.
헤더.페이로드.서명
header.payload.signature
- 헤더(header): 서명에 쓴 알고리즘(
alg)과 토큰 종류(typ)를 담은 JSON이에요. - 페이로드(payload): 사용자 식별자, 만료 시각 같은 주장(claim)을 담은 JSON이에요.
- 서명(signature): 앞의 두 덩어리가 중간에 바뀌지 않았는지 확인하는 값이에요.
서명된 JWT는 IETF 표준 RFC 7515(JWS)의 압축 직렬화 형식을 따라요. 헤더와 페이로드는 각각 base64url로 인코딩되고, 서명은 base64url(헤더) + "." + base64url(페이로드)라는 문자열을 대상으로 계산해요. base64url은 일반 Base64에서 URL에 쓰기 불편한 +와 /를 -와 _로 바꾸고, 끝의 = 패딩을 없앤 방식이에요. 예를 들어 바이트 fb ff fe는 Base64로 +//+, base64url로 -__-가 돼요.
base64url은 암호화가 아니라 표기 방식이에요. 규칙만 알면 누구나 되돌릴 수 있어서, 토큰을 가진 사람은 열쇠 없이도 헤더와 페이로드를 읽을 수 있어요.
2. 예시 토큰을 단계별로 풀어 보기
아래는 학습용으로 제가 직접 만든 토큰이에요. 비밀 키도 임의의 문자열이고, 실제 서비스와는 아무 관계가 없어요.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyLTEwNDIiLCJhdWQiOiJzaG9wLWFwaSIsImlhdCI6MTc5MDAwMDAwMCwibmJmIjoxNzkwMDAwMDAwLCJleHAiOjE3OTAwMDM2MDAsImp0aSI6ImExYjJjM2Q0Iiwicm9sZSI6Im1lbWJlciJ9.CxdKjDNI5h_WYubz0NiMOguCdOEJrlhK59rdt3tn3GY
1단계. 점으로 자르기. 점을 기준으로 세 조각을 나눠요. 첫 조각은 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9예요. 참고로 JSON은 {"로 시작하기 때문에 base64url로 바꾸면 eyJ로 시작하는 토큰이 많아요.
2단계. 헤더 디코딩. 첫 조각을 base64url로 풀면 다음과 같아요.
{"alg":"HS256","typ":"JWT"}
HS256은 비밀 키 하나로 서명하고 검증하는 HMAC-SHA256 방식이에요.
3단계. 페이로드 디코딩. 두 번째 조각을 풀면 이렇게 나와요.
{
"iss": "https://auth.example.com",
"sub": "user-1042",
"aud": "shop-api",
"iat": 1790000000,
"nbf": 1790000000,
"exp": 1790003600,
"jti": "a1b2c3d4",
"role": "member"
}
4단계. 시간 값 읽기. iat, nbf, exp는 1970-01-01 00:00:00 UTC부터 센 초 단위 값이에요. 1790000000은 UTC 2026-09-21 14:13:20, 한국 시간으로 2026-09-21 23:13:20이에요. exp는 3600초(한 시간) 뒤인 한국 시간 2026-09-22 00:13:20이라, 이 시각 이후에는 받으면 안 되는 토큰이에요. 초 단위와 밀리초 단위를 헷갈리기 쉬운데, JWT의 시각은 초 단위예요. 숫자를 날짜로 바꿀 때는 Unix Timestamp 변환기가 편해요.
5단계. 서명 확인. 세 번째 조각은 서버가 비밀 키로 계산한 서명이에요. 이 값은 사람이 읽는 내용이 아니고, 서버가 같은 키로 다시 계산해 일치하는지 볼 때만 의미가 있어요. 손으로 디코딩하는 데 base64가 필요하다면 Base64 인코딩·디코딩의 URL-safe 옵션을 쓸 수 있고, 한 번에 보고 싶다면 JWT 디코더가 세 조각을 나눠서 보여 줘요.
3. 표준 클레임 한눈에 보기
RFC 7519는 자주 쓰는 클레임 이름을 정해 두었어요. 모두 선택 사항이지만, 서비스가 검증 규칙으로 요구할 수는 있어요.
| 이름 | 뜻 | 읽는 법 |
|---|---|---|
| iss | 발급자(Issuer) | 이 토큰을 만든 쪽. 예: 인증 서버 주소 |
| sub | 주체(Subject) | 토큰이 가리키는 대상. 보통 사용자 ID |
| aud | 대상(Audience) | 이 토큰을 받아도 되는 수신자. 내가 그 수신자가 아니면 거부해야 해요 |
| exp | 만료 시각 | 이 시각 이후에는 받으면 안 됨 |
| nbf | 유효 시작 시각 | 이 시각 전에는 쓸 수 없음 |
| iat | 발급 시각 | 토큰이 만들어진 시각. 나이를 계산할 때 사용 |
| jti | 토큰 ID | 토큰마다 고유한 식별자. 재사용(replay) 방지에 활용 |
RFC는 exp와 nbf를 검사할 때 서버 시계 오차를 감안해 "몇 분 이내"의 여유를 둘 수 있다고 적어요. 서버들의 시계가 조금씩 다르기 때문이에요. role 같은 이름은 표준 클레임이 아니라 서비스가 자유롭게 추가한 값이에요.
4. 서명이 해 주는 일, 안 해 주는 일
| 구분 | 내용 |
|---|---|
| 해 주는 일 | 헤더와 페이로드가 발급 후 바뀌지 않았는지(무결성), 그리고 키를 가진 쪽이 만들었는지 확인해 줘요 |
| 안 해 주는 일 | 내용을 숨기지 않아요. 페이로드는 누구나 읽을 수 있어요 |
| 안 해 주는 일 | 토큰이 훔쳐지는 것을 막지 못해요. 가진 사람은 그 토큰으로 요청할 수 있어요 |
| 안 해 주는 일 | 만료 전에 무효로 만들어 주지 않아요. 폐기하려면 서버 쪽에 별도 장치가 필요해요 |
내용까지 숨기고 싶다면 JWE(JSON Web Encryption)라는 별도의 암호화 형식이 있어요. RFC 7519의 검증 절차도 토큰이 JWS인지 JWE인지 구분하는 단계를 두고 있어요. 하지만 일반적으로 로그인 토큰에는 애초에 민감한 정보를 넣지 않는 편이 단순하고 안전해요.
5. 흔한 실수
① 페이로드에 민감한 정보를 넣는다. 비밀번호, 주민등록번호, 카드 정보는 넣지 마세요. 사용자 ID와 권한 정도만 담고, 나머지는 서버에서 조회하세요.
② 서명을 검증하지 않고 내용만 믿는다. 디코딩은 누구나 할 수 있고, 페이로드를 바꿔서 새 토큰처럼 만드는 것도 쉬워요. 서버는 반드시 서명을 검증해야 하고, 클라이언트에서 디코딩한 값은 화면 표시용으로만 써야 해요.
③ alg: none을 허용한다. RFC 7515는 none을 "무결성 보호가 없는" 서명 없는 JWS로 정의해요. 서명 없는 토큰을 통과시키면 누구나 토큰을 만들 수 있어요. JWT 모범 사례를 정리한 RFC 8725도 라이브러리가 허용 알고리즘을 호출자가 지정하게 하고, 그 밖의 알고리즘은 쓰지 않도록 권해요.
④ exp와 aud, iss를 확인하지 않는다. 서명이 맞아도 만료된 토큰이거나 다른 서비스를 위해 발급된 토큰일 수 있어요. 만료 시각, 발급자, 대상까지 함께 검증해야 해요. RFC 8725는 iss와 aud 검증도 권장 사항으로 다뤄요.
⑤ 토큰 저장 위치를 깊이 생각하지 않는다. 브라우저에서 토큰을 어디에 두느냐는 장단점이 갈려요. MDN에 따르면 localStorage는 같은 출처의 어떤 스크립트든 읽을 수 있어서, XSS로 악성 스크립트가 실행되면 토큰을 읽어 갈 수 있어요. 반대로 HttpOnly 쿠키는 자바스크립트로 읽을 수 없어 XSS에 의한 탈취를 줄이지만, 쿠키는 요청에 자동으로 붙기 때문에 CSRF를 고려해 SameSite, Secure 속성을 함께 설정해야 해요. 어느 쪽이 무조건 정답이라기보다 서비스 구조에 맞춰 위험을 따져 고르는 문제예요. 어느 쪽이든 만료 시간을 짧게 두는 것이 도움이 돼요.
6. 토큰을 안전하게 확인하는 방법
- 운영 중인 서비스의 유효한 토큰은 가급적 아무 곳에나 붙여 넣지 마세요. 토큰을 가진 사람은 만료 전까지 그 사용자로 행세할 수 있어요.
- 가능하면 테스트 환경에서 발급한 토큰이나 이미 만료된 토큰으로 확인하세요.
- 비밀 키는 어떤 디코더에도 입력하지 마세요. 디코딩에는 키가 필요 없어요.
- PIKOO의 JWT 디코더는 붙여 넣은 토큰을 브라우저 안에서만 풀고 서버로 보내지 않아요. 다만 이 도구는 서명을 검증하지 않아요. 위조된 토큰도 그대로 풀리니, 읽기용 도구일 뿐 인증 판단에 쓰면 안 돼요. 만료 여부와 남은 시간,
exp·iat·nbf의 한국 시간 변환을 확인하는 용도로 쓰세요.
자주 묻는 질문
JWT와 세션 쿠키는 뭐가 다른가요?
세션 쿠키는 서버가 저장한 세션을 가리키는 번호표라서 내용은 서버에 있어요. JWT는 필요한 정보가 토큰 자체에 들어 있어 서버가 저장소를 조회하지 않고도 서명으로 검증할 수 있어요. 대신 발급한 토큰을 만료 전에 무효로 만들기가 어려워서, 로그아웃 처리나 탈취 대응에 별도 설계가 필요해요.
페이로드를 수정하면 어떻게 되나요?
내용을 바꾸면 서명 계산 대상이 달라져서 서버의 검증에서 실패해요. 비밀 키를 모르는 사람은 바뀐 내용에 맞는 서명을 만들 수 없기 때문이에요. 하지만 서버가 서명 검증을 빼먹으면 이 보호는 사라져요.
토큰이 만료됐는데도 서버가 받아 줘요.
서버 코드가 exp를 검사하지 않거나, 서버 시계가 크게 틀어졌을 수 있어요. 디코더로 exp 값을 날짜로 바꿔 지금 시각과 비교해 보고, 서버의 검증 옵션과 시계 설정을 확인하세요.
HS256과 RS256은 어떻게 다른가요?
HS256은 발급과 검증에 같은 비밀 키를 써서 키를 가진 모든 쪽이 토큰도 만들 수 있어요. RS256은 개인 키로 서명하고 공개 키로 검증해서, 검증하는 쪽에는 공개 키만 나눠 주면 돼요. 여러 서비스가 한 발급자의 토큰을 검증하는 구조에서 흔히 써요.
출처
- IETF RFC 7519 - JSON Web Token (JWT) (확인일: 2026-09-24)
- IETF RFC 7515 - JSON Web Signature (JWS) (확인일: 2026-09-24)
- IETF RFC 7518 - JSON Web Algorithms (JWA) (확인일: 2026-09-24)
- IETF RFC 8725 - JSON Web Token Best Current Practices (확인일: 2026-09-24)
- MDN Web Docs - Window: localStorage property (확인일: 2026-09-24)
- MDN Web Docs - Using HTTP cookies (확인일: 2026-09-24)
본 글은 일반적인 정보 제공을 위한 것이며 보안 설계의 전부를 다루지 않아요. 예시 토큰과 키는 학습용으로 만든 것이에요. 운영 환경의 실제 토큰이나 비밀 키는 어디에도 붙여 넣거나 공유하지 마시고, 서비스에 적용할 때는 사용하는 라이브러리와 프레임워크의 공식 문서를 확인하세요.