JWT (JSON Web Token)는 디지털 서명으로 보호된 JSON 객체 형태로 당사자 간에 데이터를 전송하기 위한 간결한 형식입니다. 토큰은 HMAC(대칭 키) 또는 RSA/ECDSA(비대칭 쌍)를 사용하여 서명할 수 있으며, 데이터 무결성과 신뢰성을 보장합니다. IETF RFC 7519, 2015에 따르면, JWT는 인증, 안전한 클레임 교환 및 OpenID Connect의 ID Token 형식으로 수백만 개의 애플리케이션에서 사용됩니다.
핵심 사항
JSON Web Token (JWT)은 JSON 객체 형태로 당사자 간에 정보를 전송하는 간결하고 자체 포함된 방식을 정의하는 개방형 표준(RFC 7519)입니다. JWT의 정보를 클레임이라고 합니다 — 주체(사용자) 및 추가 속성에 대한 진술입니다. 각 클레임은 키-값 쌍입니다: 사용자 식별자, 역할, 만료 시간, 발급자.
JWT가 자체 포함형이라고 불리는 이유는 검증에 필요한 모든 정보가 토큰 자체 내에 있기 때문입니다. 서버는 토큰의 유효성을 확인하기 위해 데이터베이스나 외부 저장소에 접근할 필요 없이 서명만 확인하면 됩니다. 이 특성은 JWT를 분산 시스템 및 마이크로서비스 아키텍처에 이상적으로 만듭니다. 여러 서비스가 공유 세션 저장소 없이 요청을 인증해야 하는 경우에 특히 유용합니다.
Auth0, 2025에 따르면, 65% 이상의 모바일 및 웹 애플리케이션이 API 인증의 기본 토큰 형식으로 JWT를 사용하며, 불투명 토큰 및 세션 식별자를 앞질렀습니다.
JWT는 점으로 구분된 세 부분으로 구성됩니다: header.payload.signature. 각 부분은 Base64url로 인코딩된 JSON입니다. 각 부분을 자세히 살펴보겠습니다.
헤더에는 두 개의 필수 필드가 포함됩니다: alg(알고리즘 — 서명 알고리즘) 및 typ(유형 — 토큰 유형, 항상 “JWT”). 알고리즘은 대칭형(HS256 — SHA-256을 사용한 HMAC) 또는 비대칭형(RS256 — SHA-256을 사용한 RSA, ES256 — P-256을 사용한 ECDSA)일 수 있습니다. 비대칭 알고리즘은 클라이언트가 비밀 키를 소유하지 않고도 서명을 검증할 수 있으므로 선호됩니다.
디코딩된 헤더 예시:
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-id-1"
}
페이로드에는 클레임 — 주체에 대한 진술이 포함됩니다. 클레임은 세 가지 유형으로 나뉩니다: 등록됨(iss, sub, aud, exp, nbf, iat, jti), 공개(개발자가 IANA 레지스트리에서 정의), 및 비공개(당사자 간 합의). sub(주체)는 고유 사용자 식별자입니다. exp(만료)는 토큰 만료 타임스탬프입니다. iss(발급자)는 토큰 발급자입니다.
{
"sub": "user-abc-123",
"iss": "https://auth.example.com",
"aud": "my-mobile-app",
"exp": 1812345678,
"iat": 1812342078,
"role": "premium_user"
}
서명은 비밀 키 또는 개인 키를 사용하여 헤더와 페이로드의 연결에 서명 알고리즘을 적용하여 생성됩니다. 공식: HMAC의 경우 HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret), 비대칭 알고리즘의 경우 RSASHA256(...). 수신자는 동일한 방식으로 서명을 계산하고 수신된 서명과 비교합니다 — 일치하면 데이터가 변경되지 않은 것입니다.
JWT 작업 흐름은 두 단계로 구성됩니다: 인증 서버에 의한 토큰 생성(발급)과 클라이언트 또는 리소스 서버에 의한 토큰 검증입니다. 인증 서버는 사용자의 자격 증명을 받고, 클레임이 포함된 페이로드를 생성하여 서명합니다. 결과 JWT는 로그인 요청에 대한 응답으로 또는 OAuth 2.0 / OpenID Connect 응답 본문에서 클라이언트로 전송됩니다.
모바일 애플리케이션에서 JWT는 다음과 같이 사용됩니다: 로그인 성공 후, 사용자는 JWT 형식의 액세스 토큰을 받습니다. 애플리케이션은 이를 안전한 저장소(iOS의 Keychain, Android의 EncryptedSharedPreferences)에 저장합니다. 각 API 요청 시, 애플리케이션은 Authorization: Bearer <token> 헤더를 추가합니다. API 서버는 JWT 서명을 검증하고, 클레임을 추출하여 이를 기반으로 액세스 결정을 내립니다 — 데이터베이스에 쿼리하지 않고.
Google Codelabs, 2025에 따르면, Firebase Authentication에서 JWT를 사용하면 세션 토큰에 비해 인증 서버에 대한 요청 수를 40~60% 줄입니다. 데이터가 각 마이크로서비스에서 로컬로 검증되기 때문입니다. 이는 지연 시간의 1밀리초가 사용자 경험에 영향을 미치는 고부하 아키텍처에서 특히 중요합니다. 분당 50,000개의 요청에서 JWT로 전환하면 인트로스펙션 요청을 처리하는 최대 10개의 서버 인스턴스를 절약할 수 있습니다.
JWT와 세션 토큰은 동일한 문제 — 요청 인증 — 를 해결하지만 아키텍처에서 근본적으로 다릅니다. 세션 토큰은 서버에 저장된 세션 데이터를 참조하는 임의의 식별자 문자열입니다(상태 저장). JWT는 모든 데이터를 자체적으로 포함하는 자체 포함형 토큰입니다(무상태).
| 매개변수 | JWT | 세션 토큰 |
|---|---|---|
| 데이터 저장 | 토큰 내부(자체 포함) | 서버(세션 저장소) |
| 확장성 | 공유 저장소 불필요 | 멀티 서버에 Redis/DB 필요 |
| 토큰 취소 | 복잡(블랙리스트 필요) | 간단(DB에서 세션 삭제) |
| 크기 | 큼(500~2000바이트) | 작음(16~64바이트) |
| 서명 검증 | 암호화 방식 | 없음(문자열 비교) |
JWT는 분산 시스템에서 우수합니다: 마이크로서비스가 공유 저장소 없이 로컬에서 토큰을 검증할 수 있습니다. 예를 들어, 5개의 마이크로서비스로 구성된 아키텍처에서 각 서비스는 네트워크 호출 없이 1~2ms 안에 JWT를 검증하는 반면, 세션 토큰은 요청마다 중앙 Redis 조회가 필요하여 10~30ms의 지연 시간이 추가됩니다. 그러나 JWT는 취소가 어렵습니다 — 한 번 발급되면 만료될 때까지 유효합니다. 세션 토큰은 DB 또는 Redis에서 레코드를 삭제하여 쉽게 취소할 수 있습니다.
모바일 애플리케이션의 경우, 짧은 수명(15~30분)의 JWT와 리프레시 토큰을 결합한 접근 방식이 성능과 보안 사이의 균형을 제공합니다. JWT는 API 액세스에 사용되고, 리프레시 토큰(일반적으로 불투명)은 새 JWT를 얻는 데 사용됩니다. JWT가 손상되면 공격자는 15~30분 동안 액세스할 수 있습니다. 리프레시 토큰이 손상되면, 로테이션 및 재사용 감지를 통해 세션이 차단됩니다.
JWT의 보안은 올바른 구현에 달려 있습니다. 가장 일반적인 취약점은 “alg none” 공격입니다: 공격자가 토큰의 헤더를 “alg”: “none”으로 변경하고, 서버가 알고리즘을 검증하지 않고 위조된 토큰을 수락합니다. 보호 방법: 헤더의 알고리즘이 예상된 것(RS256, ES256)과 일치하는지 항상 확인하고, alg: none이 있는 토큰을 거부합니다.
JWT 취약점에는 다음도 포함됩니다: HMAC의 약한 비밀 키(몇 분 안에 무차별 대입), 개인 키 유출(서버를 대신하여 임의의 데이터에 서명), 페이로드에 민감한 데이터 저장(JWT는 암호화하지 않고 서명만 수행), JWK 헤더 인젝션 공격(사용자 지정 공개 키 주입). 신뢰할 수 있는 라이브러리 — Nimbus JOSE + JWT, jjwt(io.jsonwebtoken), PyJWT — 를 사용하면 이러한 취약점 악용 위험을 줄일 수 있습니다.
추가 보안 조치로 JWK Thumbprint(RFC 7638)가 있습니다: 헤더의 지문(thumbprint)을 통해 공개 키를 토큰에 바인딩합니다. 서버가 각 클라이언트에 대해 예상되는 지문을 저장하면, JWK 헤더 인젝션이 불가능해집니다 — 서버는 등록된 키와 일치하지 않는 모든 키를 거부합니다. OAuth Security Workshop 2025는 금융 및 의료 애플리케이션에서 사용되는 모든 JWT에 JWK Thumbprint를 필수 보호 조치로 권장합니다.
jjwt 라이브러리(auth0/java-jwt)를 사용하면 Android 애플리케이션에서 몇 줄의 코드로 JWT를 생성하고 검증할 수 있습니다. 아래 예제에서 서버는 sub 및 role이 포함된 토큰을 생성하고, 클라이언트는 서명을 검증합니다. 서버에서 비밀 키를 안전하게 저장하려면 환경 변수 또는 HSM(하드웨어 보안 모듈)을 사용하세요 — 코드나 구성 파일에 키를 저장하는 것은 심각한 보안 오류입니다.
val secret = "my-256-bit-secret-key-here"
val token = JWT.create()
.withSubject("user-abc-123")
.withIssuer("auth.example.com")
.withClaim("role", "premium_user")
.withExpiresAt(Date(System.currentTimeMillis() + 3600000))
.sign(Algorithm.HMAC256(secret))
// 클라이언트에 토큰 전송
println("JWT: $token")
fun verifyToken(token: String): Boolean {
return try {
val decoded = JWT.require(Algorithm.HMAC256(secret))
.withIssuer("auth.example.com")
.build()
.verify(token)
// 서명이 유효함, 클레임 추출됨
println("Subject: ${decoded.subject}")
true
} catch (e: Exception) {
println("토큰이 유효하지 않음: ${e.message}")
false
}
}
자주 묻는 질문
아니요. JWT는 서명되며 암호화되지 않습니다 — 누구나 Base64 페이로드를 디코딩하여 데이터를 읽을 수 있습니다. 민감한 정보(비밀번호, 카드 번호, 개인 데이터)는 JWE(JSON Web Encryption)를 사용하여 암호화된 형태로만 전송해야 합니다.
ES256(P-256을 사용한 ECDSA)이 권장됩니다 — 훨씬 작은 서명 크기로 RSA 2048비트와 동등한 보안 수준을 제공합니다. RS256은 레거시 시스템과의 호환성에 적합합니다. HS256(HMAC)은 비밀 키의 안전한 교환이 필요하며, 분산 아키텍처에서 더 어렵습니다.
JWT를 직접 취소할 수는 없습니다 — exp까지 유효합니다. 해결책: 짧은 수명(15~30분) 사용, 서버에서 취소된 jti(JWT ID)의 블랙리스트 유지, 또는 토큰을 비밀 키 버전에 바인딩합니다. 리프레시 토큰은 표준 방식 — 저장소에서 제거 — 으로 취소됩니다.
Bearer 토큰은 개념입니다: 소유자가 액세스에 사용할 수 있는 모든 토큰입니다. JWT는 특정 토큰 형식입니다. Bearer 토큰은 JWT일 수도 있고 불투명 문자열일 수도 있습니다. JWT는 Bearer 개념에 자체 포함성과 암호화 검증을 추가합니다.
RS256 서명이 있는 일반적인 JWT는 500~2000바이트입니다. 페이로드에 많은 사용자 지정 클레임이 포함되거나 큰 키를 사용한 비대칭 서명이 사용되는 경우 크기가 4~5KB에 도달할 수 있습니다. 이는 세션 토큰(16~64바이트)보다 훨씬 크며, HTTP 헤더 크기에 영향을 미칩니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.