본문 바로가기

개발자 최종 업데이트 2026-09-12 · 약 8분

JWT는 암호화가 아니다 — 구조, 클레임, 그리고 흔한 오해

JWT의 세 조각 구조와 시간 클레임을 정리하고, 페이로드가 누구나 읽힌다는 사실·서명 검증 위치·토큰 저장 방식·즉시 무효화의 어려움을 설명합니다.

JWT는 로그인 상태를 유지하는 가장 흔한 방법 중 하나가 되었습니다. 그런데 구조를 정확히 모른 채 쓰다 보면 위험한 오해를 하기 쉽습니다. 가장 흔한 것이 "토큰은 암호화되어 있으니 안에 뭘 넣어도 안전하다"는 생각인데, 이는 사실이 아닙니다. 이 글에서는 JWT가 어떻게 생겼고 무엇을 보장하며 무엇은 보장하지 않는지를 정리합니다.

세 조각으로 이루어진 문자열

JWT는 점(.)으로 구분된 세 부분으로 되어 있습니다.

헤더.페이로드.서명

  • 헤더(Header) — 어떤 알고리즘으로 서명했는지, 토큰 종류가 무엇인지를 담습니다. 보통 {"alg":"HS256","typ":"JWT"} 같은 모습입니다.
  • 페이로드(Payload) — 실제 담고 싶은 정보입니다. 사용자 ID, 권한, 만료 시각 등이 들어갑니다. 여기 들어가는 항목 하나하나를 클레임(claim)이라 부릅니다.
  • 서명(Signature) — 앞의 두 조각을 비밀키로 계산한 값입니다. 내용이 바뀌면 이 값이 맞지 않게 되어 위변조를 알아챌 수 있습니다.

가장 중요한 사실: 암호화가 아니다

앞의 두 조각은 Base64URL로 인코딩되어 있을 뿐입니다. 인코딩은 감추는 것이 아니라 옮기기 좋은 형태로 바꾸는 것이라, 토큰을 가진 사람은 누구나 그 자리에서 내용을 읽을 수 있습니다. 브라우저 개발자 도구에 토큰을 붙여넣고 atob()를 실행하면 바로 풀립니다.

그래서 페이로드에 비밀번호, 주민등록번호, 카드번호 같은 값을 넣으면 안 됩니다. JWT가 제공하는 것은 기밀성이 아니라 무결성입니다. 내용을 감추지는 못하지만, 누가 내용을 고치면 서명이 맞지 않아 서버가 거부할 수 있습니다. 사용자 ID나 권한 이름처럼 노출되어도 무방한 값만 담는 것이 원칙입니다.

시간 관련 클레임

클레임 형식
exp 만료 시각. 이 시각이 지나면 무효 Unix 타임스탬프(초)
iat 발급 시각 Unix 타임스탬프(초)
nbf 이 시각 이전에는 사용 불가 Unix 타임스탬프(초)
sub 토큰의 주체(보통 사용자 ID) 문자열
iss 발급자 문자열

exp밀리초가 아니라 초라는 점을 놓치면 만료가 1,000배 뒤로 밀리거나 즉시 만료되는 버그가 생깁니다. 자바스크립트의 Date.now()는 밀리초를 돌려주므로 1,000으로 나눠야 합니다. 타임스탬프를 사람이 읽는 시각으로 바꿔 보려면 Unix 타임스탬프 변환기를 쓰면 됩니다.

서명 검증은 어디서 해야 하나

서명 검증에는 발급 서버가 가진 비밀키(HS256 계열) 또는 공개키(RS256 계열)가 필요합니다. 비밀키는 절대 클라이언트로 내려보내면 안 되므로, 검증은 반드시 서버에서 이루어져야 합니다. 브라우저에서 토큰을 디코딩해 권한을 확인하는 것은 화면을 그리기 위한 편의일 뿐, 보안 장치가 될 수 없습니다. 실제 권한 검사는 매 요청마다 서버가 다시 해야 합니다.

이와 관련해 알려진 공격도 있습니다. 헤더의 algnone으로 바꿔 서명 없이 통과시키려는 시도인데, 서버가 토큰이 주장하는 알고리즘을 그대로 믿으면 뚫립니다. 검증할 때는 서버가 기대하는 알고리즘을 고정해 두어야 합니다.

토큰을 어디에 보관할까

정답이 하나로 정해져 있지 않고 위협 모델에 따라 갈립니다.

  • localStorage — 쓰기 쉽지만 자바스크립트로 읽히므로, XSS 취약점이 하나라도 있으면 토큰이 통째로 유출됩니다.
  • HttpOnly 쿠키 — 자바스크립트가 읽을 수 없어 XSS에 강합니다. 대신 CSRF 대비가 필요하며, SameSite 속성과 CSRF 토큰을 함께 씁니다.
  • 메모리(변수) — 가장 안전하지만 새로고침하면 사라지므로 리프레시 토큰과 조합해야 합니다.

많은 서비스가 짧은 수명의 액세스 토큰을 메모리나 헤더로 다루고, 긴 수명의 리프레시 토큰을 HttpOnly 쿠키에 두는 방식을 씁니다.

JWT의 약점: 즉시 무효화가 어렵다

세션을 서버에 저장하는 방식은 로그아웃 시 서버에서 세션을 지우면 끝입니다. 그런데 JWT는 서버가 상태를 갖지 않는다는 것이 장점이자 한계라서, 이미 발급된 토큰을 만료 전에 무효화하기가 까다롭습니다. 비밀번호를 바꾸거나 계정을 정지시켜도 기존 토큰은 exp까지 유효합니다.

그래서 실무에서는 액세스 토큰의 수명을 짧게(보통 5~30분) 두고, 리프레시 토큰으로 갱신하는 구조를 씁니다. 즉시 차단이 꼭 필요하다면 차단 목록 (블랙리스트)을 별도로 두는데, 이렇게 되면 무상태라는 장점이 상당 부분 사라집니다. 모든 요청이 아니라 민감한 작업에만 확인하는 식으로 절충하기도 합니다.

디버깅할 때의 주의사항

JWT 디코더는 토큰을 브라우저 안에서만 풀어 보여 주지만, 그렇다 해도 운영 중인 실제 토큰을 아무 도구에나 붙여넣는 습관은 권하지 않습니다. 토큰은 그 자체로 로그인 자격이라, 화면을 공유하는 중이거나 클립보드가 여러 기기에 동기화되는 환경이라면 그대로 새어 나갈 수 있습니다. 확인이 필요하면 만료된 토큰이나 개발 환경 토큰을 쓰세요.

마무리

JWT를 다룰 때 기억할 것은 세 가지입니다. 앞 두 조각은 누구나 읽을 수 있다는 것, 서명 검증은 서버에서 해야 한다는 것, 그리고 발급한 토큰은 만료 전에 되돌리기 어렵다는 것입니다. 이 셋을 전제로 설계하면 대부분의 사고를 피할 수 있습니다. 토큰 내용을 확인하려면 JWT 디코더를, 인코딩된 값을 직접 풀어 보려면 Base64 디코더를 사용해 보세요.

이 글과 관련된 도구