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

JWT는 점(.)으로 구분되는 세 부분으로 구성되며, 각 부분은 다음과 같다:
- 헤더 (Header)
- 페이로드 (Payload)
- 서명 (Signature)
따라서 JWT는 일반적으로 xxxxx.yyyyy.zzzzz 형태를 띤다.
헤더(Header)
헤더는 일반적으로 두 부분으로 구성된다: 토큰의 유형(즉, JWT)과 사용되는 서명 알고리즘(예: HMAC SHA256 또는 RSA)이다.
예를 들면 다음과 같다.
{
"alg": "HS256",
"typ": "JWT"
}
그런 다음 이 JSON은 JWT의 첫 번째 부분을 구성하기 위해 Base64Url로 인코딩된다.
페이로드(Payload)
토큰의 두 번째 부분은 페이로드이며, 여기에는 전달하고자 하는 주요 정보 항목들(이를 기술 용어로는 클레임이라고도 한다)이 포함된다. 이러한 정보 항목들은 주체(일반적으로 사용자)에 대한 설명이나 추가적인 메타데이터이다. 이 정보 항목에는 세 가지 유형이 있다: 등록된 정보 항목, 공개 정보 항목, 그리고 비공개 정보 항목이다.
- 등록된 정보 항목 (Registered Claims): 미리 이름과 의미가 정의되어 있는 정보 항목들이다. 필수는 아니지만 권장되며, 유용하고 상호 운용 가능한 정보 집합을 제공한다. 일부 예로는 iss (발급자), exp (만료 시간), sub (주제), aud (수신자) 등이 있다.
- 주의: 정보 항목의 이름은 간결성을 위해 세 글자로만 이루어진다.
- 공개 정보 항목 (Public Claims): JWT를 사용하는 사람들이 필요에 따라 자유롭게 정의할 수 있는 정보 항목이다. 그러나 다른 곳에서 사용 중인 이름과 충돌하는 것을 피하기 위해, IANA JSON 웹 토큰 레지스트리에 정의하거나, 충돌 방지 네임스페이스를 포함하는 URI로 이름을 정의해야 한다.
- 비공개 정보 항목 (Private Claims): 정보를 공유하기로 동의한 당사자들 간에 생성되는 사용자 정의 정보 항목이다. 등록된 정보 항목이나 공개 정보 항목의 이름과 충돌하지 않도록 주의해야 한다.
- 주의: 기본적으로 Base64Url 를 통해서 인코딩 될 뿐 암호화 되는것이 아니다. 때문에, 민감정보를 JWT 에 넣어야 한다면 암호화해서 넣어야 한다.
(넣지 않는 것이 좋을 것 같다..)
- 주의: 기본적으로 Base64Url 를 통해서 인코딩 될 뿐 암호화 되는것이 아니다. 때문에, 민감정보를 JWT 에 넣어야 한다면 암호화해서 넣어야 한다.
페이로드 예시 (페이로드에 담기는 정보 항목들의 예):
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
그런 다음 페이로드 JSON은 JWT의 두 번째 부분을 구성하기 위해 Base64Url로 인코딩된다.
주의: 서명된 토큰의 경우, 정보가 변조로부터 보호되지만 누구나 읽을 수 있다는 점에 유의해야 한다. 민감한 정보를 토큰의 페이로드나 헤더 요소의 정보 항목으로 넣지 않도록 한다. 이것이 필요한 경우 암호화된 토큰을 고려해볼 수 있다.
서명(Signature)
서명 부분을 만들려면 인코딩된 헤더, 인코딩된 페이로드, 비밀키, 헤더에 지정된 알고리즘을 가져와 서명해야 한다.
예를 들어 HMAC SHA256 알고리즘을 사용하려면 서명은 다음과 같은 방식으로 생성된다:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
서명은 메시지가 도중에 변경되지 않았는지 확인하는 데 사용되며, 비밀키로 서명된 토큰의 경우 발신자가 누구인지도 확인할 수 있다.
JWT 는 어떻게 동작하는가?
인가(Authorization)에서 사용자가 자격 증명을 사용하여 성공적으로 로그인하면 JWT 가 반환된다. 토큰은 자격 증명이므로 보안 문제를 방지하기 위해 매우 신중하게 처리해야 한다. 일반적으로 토큰을 너무 오랫동안 보관해서는 안 된다.
사용자가 보호된 경로 또는 리소스에 접근하려고 할 때마다, 일반적으로 Bearer 스키마를 사용하여 Authorization 헤더에 JWT를 보내야 한다. 헤더의 내용은 다음과 같아야 한다:
Authorization: Bearer <token>
이는 특정 경우에 상태 비저장(stateless) 인가 메커니즘이 될 수 있다. 서버의 보호된 경로는 Authorization 헤더에서 유효한 JWT가 있는지 확인하고, 있다면 사용자는 보호된 리소스에 접근할 수 있게 된다. JWT에 필요한 정보(데이터 항목들)가 포함되어 있다면, 특정 작업을 위해 데이터베이스를 여러 번 조회해야 하는 필요성이 줄어들 수 있다.
토큰이 Authorization 헤더로 전송되면, 교차 출처 리소스 공유(CORS)가 문제가 되지 않는데, 이는 쿠키를 사용하지 않기 때문이다.
클라이언트 - 서버 JWT 사용 예시
1단계: 사용자 로그인 및 자격 증명 제출
- 사용자: 웹사이트나 앱에서 아이디와 비밀번호 같은 로그인 정보를 입력하고 서버로 전송한다 (로그인 요청).
- 서버: 전달받은 로그인 정보를 데이터베이스에 저장된 사용자 정보와 비교하여 유효한 사용자인지 확인한다.
2단계: 서버의 JWT 생성 및 발급 (가장 핵심적인 부분)
- 헤더(Header) 준비:
- 어떤 서명 알고리즘을 사용할지(예: HS256 - HMAC SHA256)와 토큰의 타입(JWT)을 JSON 형태로 정의한다.
-
{ "alg": "HS256", "typ": "JWT" } - 이 JSON 객체를 Base64Url 방식으로 인코딩한다. 이것이 JWT의 첫 번째 부분이 된다.
- 페이로드(Payload) 준비:
-
- 토큰에 담을 실제 정보 항목들(기술 용어로는 클레임)을 JSON 형태로 정의한다. 이 정보에는 다음과 같은 것들이 포함될 수 있다.
- sub (Subject): 토큰의 주체, 즉 사용자를 식별할 수 있는 고유 ID.
- name (Name): 사용자의 이름 (선택 사항).
- role (Role): 사용자의 역할 또는 권한 (애플리케이션 정의 비공개 정보 항목).
- iss (Issuer): 토큰 발급자 (예: mydomain.com).
- exp (Expiration Time): 토큰의 만료 시간. 이 시간이 지나면 토큰은 무효가 된다. (매우 중요!)
- iat (Issued At): 토큰이 발급된 시간.
- 주의: 비밀번호와 같은 민감 정보는 절대 여기에 포함해서는 안 된다. Base64Url 인코딩은 암호화가 아니기 때문이다.
{ "sub": "user123_id", "name": "홍길동", "role": "user", "iss": "myApiService.com", "exp": 1735689600, // 예시: 2025년 1월 1일 00:00:00 GMT "iat": 1704067200 // 예시: 2024년 1월 1일 00:00:00 GMT } - 토큰에 담을 실제 정보 항목들(기술 용어로는 클레임)을 JSON 형태로 정의한다. 이 정보에는 다음과 같은 것들이 포함될 수 있다.
- 이 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) 등의 정보를 바탕으로 어떤 사용자가 요청했는지 파악한다.
- 해당 사용자의 권한에 따라 요청된 작업을 수행한다.
- 작업 결과를 클라이언트에게 응답으로 보낸다.
왜 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를 매력적인 선택으로 만든다.
'Backend > 소소한 백엔드 개발 이야기' 카테고리의 다른 글
| 의존성 주입(DI) 란 무엇일까? (0) | 2025.07.25 |
|---|---|
| HTTPS 통신 방식 (0) | 2025.06.08 |