헤더:{"alg":"HS256","typ":"JWT"} — 알고리즘과 타입을 나타내. 페이로드:{"sub":"u_42","exp":1760000000} — 주체와 만료 시각 같은 클레임을 담아. 서명: 발급자가 관리하는 비밀 키나 개인 키로 base64(header) + "." + base64(payload)에 HMAC-SHA256 또는 RSA 서명을 적용한 결과야.
누구나 JWT의 앞 두 부분을 base64url로 디코딩해 클레임을 읽을 수 있어. 신뢰하도록 설정한 키와 허용한 알고리즘으로 서명이 검증되면, 서명 뒤 헤더와 페이로드가 바뀌지 않았고 해당 키를 가진 주체가 서명했다는 사실을 확인할 수 있어. 그 주체가 기대한 발급자인지는 키 선택과 iss 검증으로 따로 확인해야 해. 서명형 JWT는 암호문이 아니야. 페이로드에 비밀값을 넣지 마.
표준 클레임과 검증 목적
RFC 7519는 여러 등록 클레임 이름을 정의해:
iss (issuer) — 토큰을 발급한 주체의 식별자야. 검증자는 기대한 값과 정확히 비교해야 해.
sub (subject) — 토큰이 나타내는 주체로, 보통 사용자 ID를 사용해.
aud (audience) — 토큰을 받아야 하는 대상이야. 다른 서비스용 토큰이 잘못 재사용되는 일을 막아.
exp (expiry) — 이 시각이 지나면 토큰을 거부해야 해. 액세스 토큰에는 빠뜨리지 않는 게 좋아.
nbf (not before) — 이 시각 전에는 토큰을 받아들이지 말라는 뜻이야. 예약된 접근 권한에도 활용할 수 있어.
jti (JWT ID) — 토큰의 고유 식별자야. 토큰별 거부 목록이나 재사용 탐지에 유용하지만 유일한 폐기 방법은 아니야.
role이나 scope, org_id처럼 애플리케이션에 필요한 클레임도 추가할 수 있어. 다만 모든 요청에 토큰 전체가 실리고 오래된 권한 정보가 만료 전까지 남을 수 있으므로, 꼭 필요한 값만 넣어.
세 가지 핵심 보안 함정
1. alg=none. 과거 일부 JWT 라이브러리는 {"alg":"none"}으로 표시된 무서명 토큰을 검증된 토큰처럼 받아들였어. 인증용 JWT를 검증할 때는 alg: none을 허용하지 말고, 사용하는 라이브러리가 실제로 서명을 요구하는지 확인해.
2. 알고리즘 혼동. 검증 코드가 토큰 헤더의 알고리즘을 그대로 신뢰하고 같은 키 자료를 대칭·비대칭 알고리즘에 잘못 재사용하면, 공개 키가 HMAC 비밀값처럼 취급되는 혼동 공격이 가능해질 수 있어. 애플리케이션이 허용하는 알고리즘을 외부 설정으로 고정하고, 알고리즘 종류에 맞는 키만 사용해야 해.
3. 페이로드에 비밀값 넣기. 페이로드는 base64url로 인코딩될 뿐 암호화되지 않아. 토큰을 얻은 주체는 누구나 읽을 수 있어. 비밀번호나 민감한 개인정보, 내부 전용 정보를 넣지 마.
HS256과 RS256 — 신뢰 경계로 선택해
HS256 (HMAC-SHA256, 대칭): 하나의 비밀값으로 서명과 검증을 모두 해. 검증 키를 가진 서비스는 새 토큰도 만들 수 있으므로, 발급자와 검증자가 같거나 완전히 같은 신뢰 경계에 있을 때 적합해. 단일 백엔드에서는 구성이 단순해.
RS256 (RSA, 비대칭): 개인 키로 서명하고 공개 키로 검증해. 인증 서버만 개인 키를 보관하고 여러 리소스 서버에는 공개 키만 배포할 수 있어. 검증자가 토큰을 발급할 권한까지 가질 필요가 없을 때 유리해.
신원 제공업체는 RS256뿐 아니라 다른 비대칭 알고리즘도 사용할 수 있어. 실제로는 공급자의 메타데이터와 키 집합을 따르고, 허용 알고리즘·발급자·대상을 명시적으로 검증해야 해. 단일 서비스에서도 키 관리와 확장 계획에 따라 대칭 또는 비대칭 방식을 선택할 수 있어.
JWT는 무상태 검증을 가능하게 하지만, 자동으로 완성해 주지는 않아. 매 요청마다 세션 DB를 조회하지 않고도 서명과 alg·iss·aud·exp·nbf 같은 조건을 검증할 수 있어. 대신 발급 뒤 권한이 바뀌거나 토큰을 즉시 폐기해야 할 때는 검증자가 그 사실을 알 수 없어. 짧은 exp, 키 회전, 토큰 버전, jti 거부 목록, 사용자 상태 조회 가운데 구조에 맞는 전략을 선택해야 해. 단기 액세스 토큰과 서버에서 관리하는 갱신 토큰을 조합하는 방식이 흔한 타협이야.
갱신 토큰으로 세션 이어 가기
액세스 토큰을 15분처럼 짧게 두면 노출 뒤 악용 가능한 시간을 줄일 수 있어. 그렇다고 15분마다 다시 로그인하게 만들 수는 없으니, 더 긴 수명의 갱신 토큰을 별도로 발급해 새 액세스 토큰을 받게 해.
로그인에 성공하면 단기 액세스 토큰과 장기 갱신 토큰을 함께 발급해.
클라이언트는 일반 API 호출에 액세스 토큰만 사용해. 만료가 가까워지면 갱신 토큰을 /auth/refresh에 POST하고 새 액세스 토큰을 받아.
갱신 토큰은 액세스 토큰보다 더 엄격하게 보관해. 브라우저에서는 Secure·HttpOnly·적절한 SameSite 속성을 가진 쿠키와 CSRF 방어를 함께 고려하고, 네이티브 앱에서는 운영체제의 안전한 저장소를 사용해. 일반 API 요청에는 보내지 마.
로그아웃이나 침해 대응 때는 서버에서 갱신 토큰을 폐기해. 이미 발급된 액세스 토큰은 짧은 만료 시간이나 별도의 거부 전략으로 처리해.
갱신 토큰 회전은 성공한 갱신마다 새 갱신 토큰을 발급하고 이전 토큰을 무효화하는 패턴이야. 이미 사용한 토큰이 다시 나타나면 복제나 탈취를 의심해 해당 토큰 계열을 폐기할 수 있어. 다만 네트워크 재시도와 동시에 들어온 요청도 재사용처럼 보일 수 있으므로, 원자적 처리와 짧은 유예 또는 멱등성 전략을 함께 설계해야 해.
cwkPippa의 JWT 선택
cwkPippa는 JWT 대신 만료 시각과 함께 SQLite에 저장한 불투명한 무작위 Bearer 토큰을 사용하고, 요청마다 DB에서 검증해. 백엔드가 하나인 구조에서는 구현과 즉시 폐기가 단순하다는 장점이 있어. 검증 주체가 여러 서비스나 네트워크 경계로 늘어나면 JWT의 무상태 검증이 매력적일 수 있지만, 그때도 키 배포와 클레임 검증, 폐기 전략이 필요해. 유행보다 시스템의 신뢰 경계와 운영 요구에 맞는 방식을 고르는 게 중요해.
Code
직접 JWT 디코딩하기 — 헤더와 페이로드는 비밀이 아니야·python
# 라이브러리 없이 손으로 JWT decode
import base64, json
token = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1XzQyIiwiZXhwIjoxNzYwMDAwMDAwfQ.signature'
header_b64, payload_b64, sig_b64 = token.split('.')
# Base64url 이 padding 생략하니 패딩 추가
def b64url_decode(s: str) -> bytes:
return base64.urlsafe_b64decode(s + '=' * (-len(s) % 4))
header = json.loads(b64url_decode(header_b64))
payload = json.loads(b64url_decode(payload_b64))
print(header) # {'alg': 'HS256', 'typ': 'JWT'}
print(payload) # {'sub': 'u_42', 'exp': 1760000000}
# Signature 가 유일 보호 부분 — 누구든 나머지 읽음.
FastAPI로 JWT 인증 흐름을 처음부터 만들어봐. /login은 15분짜리 HS256 액세스 토큰과 30일짜리 불투명 갱신 토큰을 반환하고, /auth/refresh는 갱신 토큰을 회전하면서 새 액세스 토큰을 발급해야 해. /me는 유효한 액세스 토큰이 있을 때만 열어. 같은 갱신 토큰이 두 번 제출되면 해당 토큰 계열을 폐기하는 재사용 탐지도 추가해. 이어서 세 공격을 시험해봐. 재서명 없이 액세스 토큰 페이로드 수정하기, alg=none 토큰 제출하기, 이미 사용한 갱신 토큰 다시 제출하기야. 앞의 두 요청은 401이어야 하고, 마지막 요청은 401과 함께 계열의 다른 갱신 토큰도 무효화해야 해.
Hint
PyJWT에서는 jwt.encode(..., algorithm='HS256')로 발급하고 jwt.decode(..., algorithms=['HS256'])로 검증해. issuer와 audience, exp 같은 필수 클레임 검사도 추가해. 갱신 토큰마다 family_id를 저장하고, 회전할 때 used=True 표시와 새 토큰 발급을 하나의 원자적 작업으로 처리하면 돼. 이미 사용된 토큰이 다시 들어오면 같은 family_id의 토큰을 모두 폐기해. 세 공격이 모두 기대한 401로 끝나는지 통합 테스트로 확인해.
Progress
Progress is local-only — sign in to sync across devices.