JWT(JSON Web Token)가 무엇인지, 세 부분이 어떻게 동작하는지, 활용처, 그리고 흔한 토큰 실수를 피하기 위한 핵심 보안 팁을 알아보세요.

**JWT(JSON Web Token)**은 시스템 간에 전달할 수 있는, 정보를 담은 컴팩트한 URL-안전 문자열입니다. 보통 eyJ... 같은 긴 값으로 시작하며 Authorization: Bearer <token> 같은 HTTP 헤더에 담겨 전송됩니다.
전통적인 로그인은 서버 세션에 의존합니다. 사용자가 로그인하면 서버가 세션 데이터를 저장하고 브라우저에 세션 ID 쿠키를 줍니다. 모든 요청에 그 쿠키가 포함되고 서버는 세션을 조회합니다.
토큰 기반 인증에서는 서버가 모든 사용자 요청에 대해 상태를 유지할 필요가 없습니다. 대신 클라이언트가 JWT 같은 토큰을 보관하고 API 호출 시 포함합니다. API 환경에서 선호되는 이유:
중요한 차이: “무상태”라 해서 서버 측 검사를 전혀 하지 않는다는 의미는 아닙니다. 많은 시스템은 여전히 사용자 상태, 키 회전, 폐기 메커니즘 등을 확인합니다.
JWT는 흔히 인증 증거(로그인 상태)와 기본적인 인가 힌트(역할, 권한, 스코프)를 담지만, 실제 인가 규칙은 서버에서 반드시 적용해야 합니다.
JWT는 보통 액세스 토큰으로 사용됩니다:
JWT는 세 부분으로 구성된 컴팩트한 문자열이며, 각 부분은 Base64URL로 인코딩되고 점(.)으로 구분됩니다:
header.payload.signature
예시(축약):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNzAwMDAwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c…
헤더는 토큰이 어떻게 만들어졌는지를 설명합니다—특히 서명 알고리즘(예: HS256, RS256/ES256)과 토큰 타입이 중요합니다.
일반 필드:
typ: 보통 "JWT"(실무에서는 무시되는 경우가 많음)alg: 사용된 서명 알고리즘kid: 키 식별자(키 회전 시 검증자가 올바른 키를 선택하도록 도움)보안 노트: 헤더를 그대로 신뢰하지 마세요. 실제 사용하는 알고리즘을 허용 목록으로 제한하고 alg: "none"는 절대 수락하지 마세요.
페이로드는 사용자와 토큰 컨텍스트에 관한 “클레임”(필드)을 담습니다: 누구를 위한 토큰인지, 누가 발행했는지, 언제 만료되는지 등.
중요: JWT는 기본적으로 암호화되지 않습니다. Base64URL 인코딩은 토큰을 URL 안전하게 만들 뿐 데이터를 숨기지 않습니다. 토큰을 가진 사람은 헤더와 페이로드를 누구나 디코드하여 볼 수 있습니다.
따라서 비밀번호, API 키 같은 비밀이나 민감한 개인 정보를 JWT에 넣지 마세요.
서명은 헤더 + 페이로드를 키로 서명하여 생성합니다:
서명은 무결성을 제공합니다: 토큰이 수정되지 않았고 신뢰할 수 있는 발행자가 발행했음을 검증할 수 있습니다. 단, 기밀성은 제공하지 않습니다.
JWT는 헤더와 페이로드를 매 요청마다 포함하므로 토큰이 클수록 대역폭과 오버헤드가 커집니다. 클레임을 간결하게 유지하고 큰 데이터를 직접 담지 마세요.
클레임은 일반적으로 등록된(표준) 클레임과 커스텀 클레임으로 나뉩니다.
iss (issuer): 토큰 발행자sub (subject): 토큰의 대상(종종 사용자 ID)aud (audience): 토큰의 대상 서비스(예: 특정 API)exp (expiration time): 토큰이 더 이상 수락되지 않아야 할 시간iat (issued at): 토큰 발급 시간nbf (not before): 이 시간 이전에는 수락하지 말아야 함수신 서비스가 실제로 인가 결정을 내리는 데 필요한 정보만 포함하세요.
좋은 예:
user_id)프로필 데이터를 중복으로 담는 “편의성 클레임”은 토큰을 부풀리고 금방 오래되어 유출 시 영향이 커집니다.
페이로드는 읽을 수 있으므로 다음을 넣지 마세요:
민감한 정보가 필요하면 서버에 저장하고 토큰에는 참조 ID만 넣거나, 필요 시 암호화된 토큰 형식(JWE)을 사용하세요.
서명은 암호화가 아닙니다.
토큰 발행 시 서버는 인코딩된 헤더 + 페이로드에 서명합니다. 나중에 토큰이 제시되면 서버는 서명을 재계산해 비교합니다. 한 글자만 바꿔도(예: "role":"user" → "role":"admin") 검증이 실패하여 토큰이 거부됩니다.
JWT는 토큰 형식이고 OAuth 2.0과 OpenID Connect(OIDC)는 토큰을 요청·발행·사용하는 방법을 설명하는 프로토콜입니다.
OAuth 2.0은 주로 인가에 관한 것입니다: 앱이 사용자의 비밀번호를 공유하지 않고 API에 접근할 수 있게 하는 방식입니다.
액세스 토큰은 보통 수명이 짧음(수 분). 짧은 수명은 토큰 유출 시 피해를 제한합니다.
OIDC는 OAuth 2.0에 인증을 추가하고 보통 JWT 형식의 ID 토큰을 도입합니다.
핵심 규칙: ID 토큰을 API 호출에 쓰지 마세요.
실무 흐름 예시는 /blog/jwt-authentication-flow 를 참고하세요.
전형적인 흐름은 다음과 같습니다:
사용자가 로그인(이메일/비밀번호, SSO 등)하면 서버는 sub, exp 같은 필수 클레임을 포함한 JWT(대개 액세스 토큰)를 생성합니다.
서버는 토큰에 서명해 클라이언트(웹 앱, 모바일 앱, 다른 서비스)에 반환합니다.
보호된 엔드포인트에 대해 클라이언트는 Authorization 헤더에 JWT를 포함합니다:
Authorization: Bearer <JWT>
요청을 처리하기 전에 API는 일반적으로 다음을 확인합니다:
exp(만료 여부)iss(예상 발행자)aud(이 API를 위한 토큰인지)모든 검사가 통과하면 API는 사용자를 인증된 것으로 간주하고 인가 규칙(예: 레코드 단위 권한)을 적용합니다.
시스템 클럭은 흐트러질 수 있으므로 exp나 nbf 같은 시간 기반 클레임 검증 시 소량의 **시계 오차(clock skew)**를 허용하는 시스템이 많습니다. 단, 스큐를 너무 크게 잡으면 토큰 유효 기간이 의도보다 길어질 수 있으니 작게 유지하세요.
저장 방식에 따라 공격자가 토큰을 어떻게 탈취할 수 있고 재사용하기 쉬운지가 달라집니다.
**메모리 저장(종종 SPA에 권장)**은 액세스 토큰을 JS 상태에 보관합니다. 새로고침 시 지워져 장기 도난 위험을 줄이지만, 활성 XSS가 있으면 여전히 읽힐 수 있습니다. 짧은 수명의 액세스 토큰과 리프레시 흐름과 함께 사용하세요.
localStorage/sessionStorage는 편하지만 위험합니다: XSS 취약점이 있으면 웹 저장소에서 토큰을 유출할 수 있습니다. 사용 시 XSS 방어를 철저히 하고 토큰을 짧게 유지하세요.
**Secure 쿠키(웹에서 종종 가장 안전한 기본값)**는 HttpOnly 쿠키에 토큰을 담아 JS가 읽지 못하게 하여 XSS로 인한 토큰 탈취 영향을 줄입니다. 단점은 브라우저가 쿠키를 자동으로 전송하므로 CSRF 위험이 생깁니다.
쿠키를 사용할 경우 다음을 설정하세요:
HttpOnlySecure(HTTPS 전용)SameSite=Lax 또는 SameSite=Strict(특정 크로스 사이트 흐름에선 SameSite=None; Secure 필요)상태 변경 요청에는 CSRF 토큰을 고려하세요.
iOS/Android에서는 Keychain / Keystore 등 OS가 제공하는 보안 저장소에 토큰을 보관하세요. 일반 파일이나 preferences에 저장하지 마세요. 루팅/탈옥 기기를 염두에 둔 위협 모델이라면 추출 가능성을 가정하고 짧은 수명 토큰과 서버 측 제어에 의존하세요.
토큰이 할 수 있는 일을 제한하세요: 최소한의 스코프/클레임만 부여하고 액세스 토큰을 짧게 유지하며 민감한 데이터를 토큰에 담지 마세요.
JWT는 편리하지만 많은 사고는 예측 가능한 실수에서 발생합니다. JWT를 현금처럼 다루세요: 획득한 사람은 대개 그걸로 권한을 행사할 수 있습니다.
토큰이 며칠 또는 몇 주 동안 유효하면 유출 시 공격자는 그 기간 동안 사용할 수 있습니다. 액세스 토큰은 가능한 짧게(수 분) 유지하고 더 긴 세션은 리프레시 토큰과 서버 측 제어로 처리하세요.
iss와 aud 검증 건너뛰기서명이 유효하다고 해서 충분하지 않습니다. **iss**와 **aud**를 검증하고 exp와 nbf 같은 시간 기반 클레임을 검사하세요.
디코딩은 검증이 아닙니다. 서버에서 항상 서명을 검증하고 인가를 강제하세요.
JWT를 쿼리 파라미터에 넣지 마세요. 브라우저 히스토리, 서버 로그, 분석 도구, 리퍼러 헤더에 남을 수 있습니다. 대신 Authorization: Bearer ... 헤더를 사용하세요.
키와 토큰이 유출될 수 있음을 가정하세요. 서명 키를 회전하고 kid를 사용해 원활한 회전을 지원하며, 폐기 전략(짧은 만료 + 계정/세션 비활성화 기능)을 마련하세요. 저장 지침은 /blog/where-to-store-jwts-safely 를 참고하세요.
JWT는 유용하지만 항상 최선의 선택은 아닙니다. 자체적으로 검증 가능한 토큰(데이터를 포함하고 데이터베이스 조회 없이 검증 가능)이 필요한지 판단하세요.
전통적인 서버 렌더링 웹 앱으로 즉시 무효화가 필요하면 서버 사이드 세션 + HttpOnly 쿠키가 더 단순하고 안전한 기본값인 경우가 많습니다.
토큰을 짧게 유지할 수 있고 서비스 간 무상태 검증이 필요하면 JWT를 사용하세요. 즉시 폐기가 필요하거나 토큰에 민감한 데이터를 담아야 하거나 세션 쿠키로도 충분하다면 JWT는 피하세요.
올바른 키와 허용된 알고리즘으로 검증하세요. 유효하지 않은 서명은 무조건 거부합니다.
exp(만료)토큰이 만료되지 않았는지 확인하세요.
nbf(유효 시작)있다면 너무 이르게 사용되는지 확인하세요.
aud(대상)토큰이 귀하의 API/서비스를 위한 것인지 확인하세요.
iss(발행자)예상 발행자인지 확인하세요.
토큰 형식 검증, 최대 크기 강제, 예기치 않은 클레임 타입 거부로 엣지 케이스 버그를 줄이세요.
HS256(대칭 키): 하나의 공유 비밀로 서명과 검증을 모두 함.
RS256/ES256(비대칭 키): 개인 키로 서명하고 공개 키로 검증.
실무 규칙: 여러 독립 시스템이 토큰을 검증해야 하거나 모든 검증자를 완전히 신뢰하지 않는다면 RS256/ES256을 선호하세요.
iss, aud, 사용자 ID 등 정책에 허용되는 범위)를 로깅하세요.JWT는 암호화되나요?
기본적으로 아니며 대부분의 JWT는 서명되며 암호화되지 않습니다. 내용은 토큰을 가진 누구나 읽을 수 있습니다. 기밀이 필요하면 JWE를 사용하거나 민감 데이터를 JWT에 넣지 마세요.
JWT를 폐기(리보크)할 수 있나요?
순수한 자체 포함형 액세스 토큰만으로는 쉽지 않습니다. 일반적인 방법은 짧은 수명의 액세스 토큰, 고위험 이벤트에 대한 차단 목록(deny-list), 또는 회전 가능한 리프레시 토큰을 사용하는 것입니다.
exp는 얼마나 길어야 하나요?
사용자 경험과 아키텍처가 허용하는 한 가능한 짧게 설정하세요. 많은 API는 액세스 토큰을 몇 분 단위로 사용하고 장기 세션은 리프레시 토큰으로 처리합니다.
JWT 인증을 새 API나 SPA에 구현할 때는 미들웨어 연결, iss/aud/exp 검증, 쿠키 플래그 설정, 로그에서 토큰 제외 같은 반복 작업이 많습니다.
Koder.ai를 사용하면 채팅 기반 워크플로로 웹 앱(React), 백엔드(Go + PostgreSQL), 또는 Flutter 모바일 앱을 빠르게 코드화하고 보안 설정, 키 회전 전략, 배포/호스팅 설정(커스텀 도메인 포함)을 관리하면서 소스 코드를 내보낼 수 있습니다. 보안 로직과 검증 흐름을 유지하면서 JWT 기반 인증 흐름을 가속화하는 실용적인 방법입니다.
JWT(JSON Web Token)는 클레임(데이터 필드)을 담아 서버에서 검증할 수 있는 컴팩트한 URL-안전 문자열입니다. 일반적으로 API 요청에서 다음과 같이 전송됩니다:
Authorization: Bearer <token>핵심은 서버가 각 요청마다 별도의 세션 레코드를 조회하지 않고도 토큰의 무결성(서명)을 검증할 수 있다는 점입니다.
세션 인증은 보통 서버에 상태(세션 레코드)를 저장하고 브라우저에 세션 ID 쿠키를 발급합니다. 반면 JWT 기반 인증에서는 클라이언트가 서명된 토큰을 매 요청 제시하고 API가 이를 검증합니다.
JWT는 검증을 로컬에서 수행할 수 있어 API와 다중 서비스 아키텍처에서 인기가 높습니다. 다만 “무상태(stateless)”라 하더라도 실제 시스템은 종종 폐기 목록, 사용자 상태 확인, 키 회전 같은 서버 측 검사를 병행합니다.
JWT는 점으로 구분된 세 개의 Base64URL 인코딩 부분으로 이루어집니다:
header.payload.signature헤더는 서명 방식 등을 설명하고, 페이로드는 sub, exp, aud 같은 클레임을 담으며, 서명은 변조를 감지하게 해줍니다.
아닙니다. 표준 JWT는 일반적으로 서명되며 암호화되지는 않습니다.
기밀성이 필요하면 JWE(암호화된 토큰)를 고려하거나 민감 데이터를 서버에 보관하고 JWT에는 식별자만 넣으세요.
서명은 토큰이 변경되지 않았고 서명 키를 가진 발행자가 만들었음을 확인해 줍니다.
그러나 서명은 다음을 보장하지 않습니다:
exp 이전에 자동으로 폐기되지 않음토큰은 자격 증명처럼 다뤄야 합니다: 유출되면 만료될 때까지 재사용될 수 있습니다.
alg는 어떤 알고리즘으로 서명되었는지를, kid는 키 식별자를 나타내어 키 회전 시 올바른 키를 선택하도록 돕습니다.
보안 권장사항:
alg 값을 수락하지 마세요.alg: "none"를 절대 허용하지 마세요.먼저 표준 등록 클레임으로 시작하고 커스텀 클레임은 최소화하세요.
일반적인 등록 클레임:
iss (issuer)JWT는 토큰 포맷이고 OAuth 2.0과 OpenID Connect는 프로토콜입니다.
일반적인 대응:
중요: ID 토큰을 API 호출에 사용하지 마세요. 용도가 다릅니다.
브라우저 기반 앱의 일반적인 선택:
쿠키를 사용한다면 다음을 설정하세요:
API가 JWT를 검증할 때 최소한 다음을 확인해야 합니다:
exp(만료 여부)iss(예상 발행자)aud(이 API를 위한 토큰인지)nbf(있다면 사용 시작 시간)추가 실무적 방어막:
kid가 안전하지 않은 키 조회로 이어지지 않도록 하세요.sub (subject / 사용자 식별자)aud (audience / 대상 API)exp (만료)iat (발급 시간)nbf (유효 시작 시간)민감한 비밀번호나 API 키 등은 페이로드에 넣지 마세요. 페이로드는 노출되기 쉽습니다.
HttpOnlySecure(HTTPS 전용)SameSite=Lax 또는 SameSite=Strict(크로스 사이트 흐름 필요 시 SameSite=None; Secure)또한 상태 변경 요청에는 CSRF 토큰을 고려하세요. 전반적으로 액세스 토큰은 짧게 유지하고 최소 권한 원칙을 적용하세요.