JWT(JSON Web Token)
참고 링크 : https://jwt.io/introduction
1. JWT 소개
JSON 웹 토큰(JWT)은 RFC 7519 공개 표준으로, 당사자 간에 정보를 JSON 객체 형태로 안전하게 전송하기 위한 간결하고 자기완결적인(compact and self-contained) 방법을 정의한다. 이 정보는 디지털 서명되어 있기 때문에 그 진위를 확인하고 신뢰할 수 있다.
JWT는 비밀키(HMAC 알고리즘 사용) 또는 RSA나 ECDSA 알고리즘을 사용하는 공개키/개인키 쌍을 이용하여 서명될 수 있다.
JWT가 당사자 간의 비밀성(secrecy)을 제공하기 위해 암호화될 수도 있지만, 여기서는 서명된 토큰에 중점을 둔다. 서명된 토큰은 그 안에 포함된 정보 항목(또는 내용, 기술 용어로는 클레임이라고도 한다)의 무결성(integrity)을 검증할 수 있는 반면, 암호화된 토큰은 다른 당사자로부터 해당 정보 항목들을 숨긴다. 토큰이 공개키/개인키 쌍을 사용하여 서명될 때, 해당 서명은 개인키를 보유한 당사자만이 서명자임을 인증(certifies)한다.
2. JWT 를 언제 사용해야 하는가?
다음은 JWT가 유용한 몇 가지 시나리오다:
인가 (Authorization): 가장 일반적인 시나리오다. 사용자가 로그인하면, 이후의 각 요청에 JWT가 포함되어 사용자가 해당 JWT가 허용하는 경로, 서비스 및 리소스에 접근할 수 있게 된다. 싱글 사인온(Single Sign On)은 오늘날 많은 기업에서 JWT를 광범위하게 사용하는 기능이다.
정보 교환 (Information Exchange): JWT는 당사자 간에 정보를 안전하게 전송하는 좋은 방법이다. 예를 들어 공개키/개인키 쌍을 사용하여 JWT에 서명함으로써, 발신자가 누구인지 확신할 수 있다. 또한 서명은 헤더와 페이로드를 사용하여 계산되므로, 정보가 변조되지 않았는지도 확인할 수 있다.
3. JWT 구조는 무엇인가?

JWT는 점(.)으로 구분되는 세 부분으로 구성되며, 각 부분은 다음과 같다:
헤더 (Header)
페이로드 (Payload)
서명 (Signature)
따라서 JWT는 일반적으로 xxxxx.yyyyy.zzzzz 형태를 띤다.
헤더(Header)
헤더는 일반적으로 두 부분으로 구성된다: 토큰의 유형(즉, JWT)과 사용되는 서명 알고리즘(예: HMAC SHA256 또는 RSA)이다.
예를 들면 다음과 같다.
그런 다음 이 JSON은 JWT의 첫 번째 부분을 구성하기 위해 Base64Url로 인코딩된다.
페이로드(Payload)
토큰의 두 번째 부분은 페이로드이며, 여기에는 전달하고자 하는 주요 정보 항목들(이를 기술 용어로는 클레임이라고도 한다)이 포함된다. 이러한 정보 항목들은 주체(일반적으로 사용자)에 대한 설명이나 추가적인 메타데이터이다. 이 정보 항목에는 세 가지 유형이 있다: 등록된 정보 항목, 공개 정보 항목, 그리고 비공개 정보 항목이다.
등록된 정보 항목 (Registered Claims): 미리 이름과 의미가 정의되어 있는 정보 항목들이다. 필수는 아니지만 권장되며, 유용하고 상호 운용 가능한 정보 집합을 제공한다. 일부 예로는 iss (발급자), exp (만료 시간), sub (주제), aud (수신자) 등이 있다.
주의: 정보 항목의 이름은 간결성을 위해 세 글자로만 이루어진다.
공개 정보 항목 (Public Claims): JWT를 사용하는 사람들이 필요에 따라 자유롭게 정의할 수 있는 정보 항목이다. 그러나 다른 곳에서 사용 중인 이름과 충돌하는 것을 피하기 위해, IANA JSON 웹 토큰 레지스트리에 정의하거나, 충돌 방지 네임스페이스를 포함하는 URI로 이름을 정의해야 한다.
비공개 정보 항목 (Private Claims): 정보를 공유하기로 동의한 당사자들 간에 생성되는 사용자 정의 정보 항목이다. 등록된 정보 항목이나 공개 정보 항목의 이름과 충돌하지 않도록 주의해야 한다.
주의: 기본적으로 Base64Url 를 통해서 인코딩 될 뿐 암호화 되는것이 아니다. 때문에, 민감정보를 JWT 에 넣어야 한다면 암호화해서 넣어야 한다.
(넣지 않는 것이 좋을 것 같다..)
페이로드 예시 (페이로드에 담기는 정보 항목들의 예):
그런 다음 페이로드 JSON은 JWT의 두 번째 부분을 구성하기 위해 Base64Url로 인코딩된다.
주의: 서명된 토큰의 경우, 정보가 변조로부터 보호되지만 누구나 읽을 수 있다는 점에 유의해야 한다. 민감한 정보를 토큰의 페이로드나 헤더 요소의 정보 항목으로 넣지 않도록 한다. 이것이 필요한 경우 암호화된 토큰을 고려해볼 수 있다.
서명(Signature)
서명 부분을 만들려면 인코딩된 헤더, 인코딩된 페이로드, 비밀키, 헤더에 지정된 알고리즘을 가져와 서명해야 한다.
예를 들어 HMAC SHA256 알고리즘을 사용하려면 서명은 다음과 같은 방식으로 생성된다:
서명은 메시지가 도중에 변경되지 않았는지 확인하는 데 사용되며, 비밀키로 서명된 토큰의 경우 발신자가 누구인지도 확인할 수 있다.
4. JWT 는 어떻게 동작하는가?
인가(Authorization)에서 사용자가 자격 증명을 사용하여 성공적으로 로그인하면 JWT 가 반환된다. 토큰은 자격 증명이므로 보안 문제를 방지하기 위해 매우 신중하게 처리해야 한다. 일반적으로 토큰을 너무 오랫동안 보관해서는 안 된다.
사용자가 보호된 경로 또는 리소스에 접근하려고 할 때마다, 일반적으로 Bearer 스키마를 사용하여 Authorization 헤더에 JWT를 보내야 한다. 헤더의 내용은 다음과 같아야 한다:
이는 특정 경우에 상태 비저장(stateless) 인가 메커니즘이 될 수 있다. 서버의 보호된 경로는 Authorization 헤더에서 유효한 JWT가 있는지 확인하고, 있다면 사용자는 보호된 리소스에 접근할 수 있게 된다. JWT에 필요한 정보(데이터 항목들)가 포함되어 있다면, 특정 작업을 위해 데이터베이스를 여러 번 조회해야 하는 필요성이 줄어들 수 있다.
토큰이 Authorization 헤더로 전송되면, 교차 출처 리소스 공유(CORS)가 문제가 되지 않는데, 이는 쿠키를 사용하지 않기 때문이다.
클라이언트 - 서버 간, JWT 사용 예시
1단계: 사용자 로그인 및 자격 증명 제출
사용자: 웹사이트나 앱에서 아이디와 비밀번호 같은 로그인 정보를 입력하고 서버로 전송한다 (로그인 요청).
서버: 전달받은 로그인 정보를 데이터베이스에 저장된 사용자 정보와 비교하여 유효한 사용자인지 확인한다.
2단계: 서버의 JWT 생성 및 발급 (가장 핵심적인 부분)
헤더(Header) 준비:
어떤 서명 알고리즘을 사용할지(예: HS256 - HMAC SHA256)와 토큰의 타입(JWT)을 JSON 형태로 정의한다.
이 JSON 객체를 Base64Url 방식으로 인코딩한다. 이것이 JWT의 첫 번째 부분이 된다.
페이로드(Payload) 준비:
토큰에 담을 실제 정보 항목들(기술 용어로는 클레임)을 JSON 형태로 정의한다. 이 정보에는 다음과 같은 것들이 포함될 수 있다.
sub (Subject): 토큰의 주체, 즉 사용자를 식별할 수 있는 고유 ID.
name (Name): 사용자의 이름 (선택 사항).
role (Role): 사용자의 역할 또는 권한 (애플리케이션 정의 비공개 정보 항목).
iss (Issuer): 토큰 발급자 (예: mydomain.com).
exp (Expiration Time): 토큰의 만료 시간. 이 시간이 지나면 토큰은 무효가 된다. (매우 중요!)
iat (Issued At): 토큰이 발급된 시간.
주의: 비밀번호와 같은 민감 정보는 절대 여기에 포함해서는 안 된다. Base64Url 인코딩은 암호화가 아니기 때문이다.
이 JSON 객체도 Base64Url 방식으로 인코딩한다. 이것이 JWT의 두 번째 부분이 된다.
서명(Signature) 생성:
앞서 만든 인코딩된 헤더와 인코딩된 페이로드를 점(.)으로 연결한다. Base64Url(Header) + "." + Base64Url(Payload)
이 연결된 문자열을, 헤더에 지정된 알고리즘(예: HS256)과 서버만 알고 있는 비밀키(Secret Key)를 사용하여 암호학적으로 해시(서명)한다.
이 비밀키는 절대 외부에 노출되어서는 안 된다. 이것이 JWT의 보안을 지키는 핵심 요소다.
이 서명 값이 JWT의 세 번째 부분이 된다.
JWT 결합 및 전송:
인코딩된 헤더, 인코딩된 페이로드, 그리고 서명을 각각 점(.)으로 연결하여 최종적인 JWT 문자열을 완성한다. Base64Url(Header) + "." + Base64Url(Payload) + "." + Signature
서버는 이 완성된 JWT를 로그인 성공 응답에 담아 클라이언트에게 전달한다.
3단계: 클라이언트의 JWT 저장
클라이언트 (브라우저/앱): 서버로부터 받은 JWT를 저장한다. 저장 위치는 다음과 같은 옵션이 있으며, 각각 보안 고려사항이 있다.
localStorage / sessionStorage: JavaScript로 접근이 가능하여 XSS(Cross-Site Scripting) 공격에 토큰이 탈취될 위험이 있다.
HTTP-only Cookie: JavaScript로 접근이 불가능하여 XSS 공격으로부터 토큰을 보호하는 데 더 유리하다. 하지만 CSRF(Cross-Site Request Forgery) 공격에 대한 방어책이 필요하다.
4단계: 보호된 리소스 요청 시 JWT 사용
클라이언트: 서버의 특정 기능(API)이나 보호된 데이터에 접근하려고 할 때, 저장해둔 JWT를 HTTP 요청의 Authorization 헤더에 담아 보낸다.
일반적으로 "Bearer"라는 인증 스킴(scheme)을 사용한다: Authorization: Bearer <여기에_JWT_문자열>
5단계: 서버의 JWT 검증 (클라이언트가 보낸 토큰 확인)
서버는 클라이언트로부터 JWT가 포함된 요청을 받으면, 이 토큰이 정말 유효한지 다음과 같이 검증한다.
토큰 추출: 요청 헤더(Authorization: Bearer <토큰>)에서 JWT 문자열을 가져온다.
구조 분리: JWT 문자열을 점(.)을 기준으로 헤더, 페이로드, 서명 부분으로 다시 분리한다.
헤더/페이로드 디코딩: 분리된 헤더와 페이로드 부분을 각각 Base64Url 디코딩하여 원래의 JSON 내용을 확인한다. 헤더에서 어떤 서명 알고리즘(alg)이 사용되었는지 확인한다.
서명 검증 (가장 중요!):
서버는 수신한 인코딩된 헤더와 인코딩된 페이로드 (즉, JWT의 첫 번째와 두 번째 부분)를 점(.)으로 다시 연결한다.
이 연결된 문자열을, 2단계에서 토큰 생성 시 사용했던 것과 동일한 비밀키(Secret Key)와 헤더에 명시된 알고리즘을 사용하여 서버 측에서 직접 서명을 다시 계산한다.
서버가 새로 계산한 서명과 클라이언트가 보낸 JWT의 원본 서명(세 번째 부분)을 비교한다.
일치하면: 토큰이 위변조되지 않았고, 해당 비밀키를 가진 신뢰할 수 있는 서버(자신)가 발급한 토큰임을 확신할 수 있다. 서명 검증 성공!
불일치하면: 토큰이 중간에 변경되었거나, 비밀키를 모르는 누군가가 위조한 가짜 토큰일 가능성이 높다. 요청은 거부된다 (예: 401 Unauthorized 또는 403 Forbidden 응답).
페이로드 정보 항목(클레임) 검증: 서명 검증에 성공했다면, 페이로드에 담긴 추가 정보들을 검증한다.
만료 시간(exp) 확인: 현재 시간이 토큰의 만료 시간을 넘었는지 확인한다. 만료되었다면 토큰은 더 이상 유효하지 않으므로 요청을 거부한다.
발급자(iss), 수신자(aud) 등 확인: 필요하다면 토큰 발급자가 예상한 발급자인지, 이 토큰이 현재 서버를 대상으로 한 것이 맞는지 등을 확인한다.
애플리케이션의 특정 로직에 따른 추가적인 검증을 수행할 수 있다.
6단계: 요청 처리 및 응답
모든 검증 과정을 통과하면, 서버는 해당 JWT가 유효하다고 판단하고 요청을 처리한다.
서버는 페이로드의 sub (사용자 ID) 등의 정보를 바탕으로 어떤 사용자가 요청했는지 파악한다.
해당 사용자의 권한에 따라 요청된 작업을 수행한다.
작업 결과를 클라이언트에게 응답으로 보낸다.
5. 왜 JWT 를 사용해야 하는가?
JSON 웹 토큰(JWT)을 사용해야 하는 이유는 다른 토큰 방식에 비해 여러 이점을 제공하기 때문이다. 특히 Simple Web Tokens (SWT) 및 Security Assertion Markup Language Tokens (SAML)과 비교했을 때 그 장점이 두드러진다.
간결성 및 크기 (SAML과의 비교):
JSON은 XML보다 덜 장황한(less verbose) 형식이므로, 인코딩되었을 때 JWT의 크기는 SAML 토큰보다 작다. 즉, JWT는 SAML보다 더 간결하다(compact).
이러한 간결성 덕분에 JWT는 HTML 및 HTTP 환경에서 전달되기에 좋은 선택이다.
보안 및 서명 방식의 유연성과 단순성:
SWT와의 비교: SWT는 주로 HMAC 알고리즘을 사용하는 공유 비밀키(shared secret)를 통해서만 대칭적으로 서명될 수 있다.
SAML과의 비교 및 JWT의 장점: 반면, JWT와 SAML 토큰은 X.509 인증서 형태의 공개키/개인키 쌍을 사용하여 서명될 수 있다. 그러나 JSON에 서명하는 것은 XML 디지털 서명(XML Digital Signature)으로 XML에 서명하는 것에 비해 훨씬 단순하며, XML 서명 시 발생할 수 있는 모호한 보안 허점을 야기할 가능성이 적다.
사용 편의성 (파싱의 용이함, SAML과의 비교):
JSON 파서는 대부분의 프로그래밍 언어에서 흔히 사용되며, JSON 데이터는 프로그래밍 언어의 객체(object)에 직접적으로 매핑된다.
반대로, XML은 자연스러운 문서 대 객체 매핑 방식이 부족하다.
이러한 차이로 인해 SAML 어설션(assertions)보다 JWT로 작업하는 것이 더 쉽다.
활용 범위 및 플랫폼 지원:
JWT는 인터넷 규모(Internet scale)에서 널리 사용된다.
이는 다양한 플랫폼, 특히 모바일 환경에서 JSON 웹 토큰의 클라이언트 측 처리가 얼마나 용이한지를 잘 보여준다.
요약하자면, JWT는 SAML에 비해 크기가 작고 처리하기 쉬우며, SWT에 비해 더 유연한 서명 옵션을 제공하면서도 SAML의 XML 서명보다 단순하고 안전하게 서명을 구현할 수 있는 이점이 있다. 또한, 광범위한 플랫폼 지원과 인터넷 규모의 서비스에서의 사용 편의성은 JWT를 매력적인 선택으로 만든다.
Last updated