Securityintermediate검토 2026.08

JWT 구조와 서명 검증

Header·Payload·Signature와 Claim 설계, Payload 위변조가 검증에서 차단되는 과정을 설명한다.

#JWT#JWS#claim#signature

Overview

일반적으로 API에서 말하는 JWT는 JSON Web Signature(JWS) Compact 형식인 header.payload.signature다. Header와 Payload는 Base64URL Encoding일 뿐 누구나 Decode할 수 있고, Signature가 발급 이후 변경되지 않았음을 검증한다.

한 문장 설명: JWT 서명은 내용을 숨기는 자물쇠가 아니라 내용이 바뀌면 검증에 실패하게 만드는 봉인이다.

왜 필요한가

API가 매 요청마다 중앙 Session Store를 조회하지 않고도 발급자, 대상 API, 사용자 식별자, 만료와 제한된 권한을 검증할 수 있다. 대신 Token이 만료되기 전까지 복제되어 사용될 수 있으므로 Claim 최소화, 짧은 수명, Key 관리가 필요하다.

Session ID가 서버 저장소의 위치를 가리키는 번호표라면, JWT는 검증 가능한 정보를 담은 서명된 문서다. 번호표 방식은 서버가 언제든 명부를 지울 수 있고, JWT 방식은 문서가 만료되기 전까지 여러 서버가 독립적으로 검증할 수 있다.

핵심 원리

Header 예시는 다음과 같다.

{ "alg": "RS256", "typ": "JWT", "kid": "2026-08-key-1" }
  • alg: 서명 알고리즘이다. Server 설정의 Allowlist와 일치해야 한다.
  • typ: Token 유형 Hint다. 보안 검증을 대신하지 않는다.
  • kid: Key Rotation 중 검증할 공개키를 찾는 식별자다. 외부 입력이므로 임의 File/URL 조회에 사용하지 않는다.

Payload의 Registered Claim은 의미가 정해져 있다.

Claim의미검증 기준
issIssuer신뢰하는 정확한 발급자
subSubject안정적인 사용자/Service ID
audAudience현재 API가 포함되는지
expExpiration현재 시간이 만료 전인지
nbfNot Before사용 시작 전이 아닌지
iatIssued At비정상 미래 발급과 수명 계산
jtiToken ID추적·재사용/폐기 식별자

내부 동작

서명 입력은 Base64URL(header) + "." + Base64URL(payload)다. HS256은 공유 Secret으로 HMAC을 계산하므로 서명자와 검증자가 같은 Secret을 안다. RS256/ES256은 Private Key로 서명하고 Public Key로 검증하므로 다수 API에 Private Key를 배포하지 않아도 된다.

flowchart LR; H[Header] --> E1[Base64URL]; P[Payload] --> E2[Base64URL]; E1 --> I[encodedHeader.encodedPayload]; E2 --> I; I --> S[Private Key/HMAC으로 Signature]; I --> V[Verifier가 재계산]; S --> C{일치?}; V --> C; C -->|Yes + Claim 정상| A[요청 허용]; C -->|No| R[401 거부]

Example

권장 Payload는 업무에 필요한 최소 식별 정보만 둔다.

{
  "iss": "https://identity.example",
  "sub": "user-8f31",
  "aud": ["orders-api"],
  "scope": "orders:read",
  "iat": 1786431600,
  "nbf": 1786431600,
  "exp": 1786432500,
  "jti": "01J..."
}

Password, 주민번호, Card 정보, 내부 Secret은 넣지 않는다. 이름·Email처럼 개인정보도 모든 요청에 필요한지 먼저 묻는다. role: admin 같은 장기 권한 Snapshot은 실제 권한 변경이 Token 만료까지 반영되지 않는 Trade-off가 있어 짧은 수명이나 Server-side 확인을 결합한다.

JWT는 어떻게 위변조되는가

공격자는 Payload를 Decode해 scope 또는 role을 바꾸고 다시 Base64URL Encoding할 수 있다. 하지만 기존 Signature는 원래 Header와 Payload로 만든 값이라 새 입력과 일치하지 않는다. 정상 검증기는 요청을 거부한다. 공격자가 유효한 Signature까지 만들려면 Secret/Private Key를 탈취하거나 검증 구현의 결함을 악용해야 한다.

과거 취약 구현의 예는 alg=none을 허용하거나, 공격자가 Header의 Algorithm을 바꿔 비대칭 Public Key를 HMAC Secret처럼 쓰게 만드는 Algorithm Confusion이다. 방어는 Token Header를 믿어 Algorithm을 자동 선택하지 않고 Server가 예상 Algorithm을 Allowlist로 고정하며, Key 유형과 Issuer/Audience를 함께 검증하는 것이다.

실무에서 발생하는 문제

Decode만 하고 Signature를 확인하지 않거나 exp만 검사하고 iss/aud를 빠뜨린다. 여러 Tenant/Issuer의 JWKS를 섞거나 kid Cache가 오래돼 Rotation 직후 실패할 수 있다. Clock Skew 허용치는 작고 명시적으로 두며, 오류 Log에 원본 Token을 남기지 않는다.

Trade-off

HS256은 단순하지만 검증자 모두가 서명 가능한 Secret을 가진다. 비대칭 방식은 권한을 분리하지만 Key Rotation과 JWKS 운영이 필요하다. Claim을 많이 넣으면 조회를 줄일 수 있으나 Token 크기, 개인정보 노출, Stale 권한이 증가한다.

Stateless 검증은 API 확장에 유리하지만 즉시 폐기에는 약하다. 그래서 Access Token은 짧게 두고, Refresh Token·Revocation·Session Version 같은 상태 기반 장치를 조합해 운영 통제력을 보완한다.

흔한 오해

  • Base64URL은 암호화가 아니다.
  • Signature는 Token 탈취와 Replay를 막지 않는다.
  • JWT라고 항상 무상태인 것은 아니다. 폐기·Refresh 관리에는 상태가 필요할 수 있다.
  • sub만 확인하면 다른 서비스용 Token을 받아들이므로 aud 검증이 필요하다.

Production Considerations

허용 Algorithm, Issuer, Audience, 최대 수명, Clock Skew를 중앙 정책으로 관리한다. Key는 Secret Manager/HSM에 두고 Rotation과 긴급 폐기를 훈련한다. 검증 실패는 이유별 Counter로 집계하되 Token/개인정보를 기록하지 않는다. TLS 없이는 Bearer Token이 전송 중 노출될 수 있다.

Interview Questions / Follow-up Questions

  1. JWT의 세 부분과 Base64URL, Signature의 역할은?
  2. Payload를 바꿀 수 있는데도 위변조를 탐지하는 원리는?
  3. iss, aud, exp를 각각 검증해야 하는 이유는?
  4. 후속: HS256과 RS256의 Key 배포 차이는?
  5. 후속: kid를 안전하지 않게 처리하면 어떤 문제가 생기는가?

인증·SAML·JWT 지도, Token 수명주기, TLS와 함께 본다.

SOURCE REFERENCES

이 문서의 근거

본문은 Dev Atlas 안에서 완결되며, 검증이 필요할 때만 원문을 확인할 수 있습니다.

원문 출처 보기 2