JWT 토큰 이해하기: 완벽 가이드
· 12분 읽기
목차
JSON 웹 토큰(JWT)이란 무엇인가?
JSON 웹 토큰(JWT)은 현대 웹 애플리케이션에서 당사자 간에 정보를 안전하게 전송하기 위한 사실상의 표준이 되었습니다. RFC 7519에 정의된 JWT는 두 당사자 간에 전송될 클레임을 표현하기 위한 컴팩트하고 URL 안전한 방법을 제공합니다.
서버가 상태를 유지하는 전통적인 세션 기반 인증과 달리, JWT는 자체 포함적입니다. 즉, 토큰 자체가 사용자의 신원과 권한을 확인하는 데 필요한 모든 정보를 담고 있습니다. 이러한 무상태 특성은 분산 시스템, 마이크로서비스 아키텍처 및 모바일 애플리케이션에서 JWT를 특히 유용하게 만듭니다.
JWT는 현대 애플리케이션에서 두 가지 주요 목적을 제공합니다:
- 권한 부여: 사용자가 로그인하면 각 후속 요청에 JWT가 포함되어 사용자가 해당 토큰으로 허용된 경로, 서비스 및 리소스에 액세스할 수 있습니다. 단일 로그인(SSO) 구현은 작은 오버헤드와 도메인 간 기능으로 인해 JWT에 크게 의존합니다.
- 정보 교환: JWT는 당사자 간에 정보를 전송하는 안전한 방법을 제공합니다. JWT는 공개/개인 키 쌍을 사용하여 서명할 수 있으므로 발신자가 주장하는 사람이 맞는지 확인하고 콘텐츠가 변조되지 않았는지 확인할 수 있습니다.
JWT의 아름다움은 단순성과 다양성에 있습니다. 다양한 프로그래밍 언어와 플랫폼에서 원활하게 작동하므로 프론트엔드는 React, 백엔드는 Node.js, 모바일 앱은 Swift 또는 Kotlin으로 구성된 이기종 환경에 이상적입니다.
전문가 팁: JWT는 매우 유용하지만 모든 인증 시나리오에 대한 만능 해결책은 아닙니다. JWT를 사용할 때와 전통적인 세션을 사용할 때를 이해하는 것은 안전하고 확장 가능한 애플리케이션을 구축하는 데 중요합니다.
JWT 구조 및 구성 요소 상세 설명
JWT는 점(.)으로 구분된 세 개의 별개 부분으로 구성되어 다음과 같은 구조를 형성합니다: xxxxx.yyyyy.zzzzz. 각 섹션은 특정 목적을 제공하며 함께 변조 방지 정보 패키지를 만듭니다.
헤더
헤더는 일반적으로 두 부분으로 구성됩니다: 토큰 유형(JWT)과 사용 중인 서명 알고리즘(예: HMAC SHA256 또는 RSA). 이 정보는 수신 측에 토큰의 서명을 검증하는 방법을 알려줍니다.
{
"alg": "HS256",
"typ": "JWT"
}
그런 다음 헤더는 Base64Url로 인코딩되어 JWT의 첫 번째 부분을 형성합니다. 일반적으로 접하게 될 알고리즘은 다음과 같습니다:
- HS256 (HMAC with SHA-256): 공유 비밀을 사용하는 대칭 알고리즘
- RS256 (RSA Signature with SHA-256): 공개/개인 키 쌍을 사용하는 비대칭 알고리즘
- ES256 (ECDSA with SHA-256): 타원 곡선 암호화를 사용하는 비대칭 알고리즘
페이로드
페이로드에는 클레임이 포함되어 있으며, 이는 엔티티(일반적으로 사용자)에 대한 진술과 추가 메타데이터입니다. 클레임은 세 가지 유형으로 분류됩니다:
등록된 클레임: 필수는 아니지만 상호 운용성을 제공하기 위해 권장되는 사전 정의된 클레임입니다. 여기에는 다음이 포함됩니다:
iss(발급자): 토큰을 발급한 사람을 식별합니다sub(주체): 토큰의 주체를 식별합니다(일반적으로 사용자 ID)aud(대상): JWT가 의도된 수신자를 식별합니다exp(만료 시간): JWT가 만료되는 타임스탬프nbf(이전 불가): JWT가 수락되어서는 안 되는 타임스탬프iat(발급 시간): JWT가 발급된 타임스탬프jti(JWT ID): JWT의 고유 식별자
공개 클레임: 애플리케이션에 대해 정의하는 사용자 정의 클레임입니다. 충돌을 피하기 위해 IANA JSON 웹 토큰 레지스트리에 정의하거나 충돌 방지 이름(네임스페이스 URI 등)을 사용해야 합니다.
비공개 클레임: 사용에 동의한 당사자 간에 정보를 공유하기 위해 생성된 사용자 정의 클레임입니다. 이들은 등록된 클레임도 공개 클레임도 아닙니다.
{
"sub": "1234567890",
"name": "John Doe",
"email": "[email protected]",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
그런 다음 페이로드는 Base64Url로 인코딩되어 JWT의 두 번째 부분을 형성합니다.
빠른 팁: JWT 페이로드에 비밀번호나 신용카드 번호와 같은 민감한 정보를 절대 저장하지 마세요. JWT는 서명되어 있지만 기본적으로 암호화되지 않으므로 누구나 페이로드를 디코딩하고 읽을 수 있습니다.
서명
서명은 인코딩된 헤더, 인코딩된 페이로드, 비밀(HMAC 알고리즘의 경우) 또는 개인 키(RSA/ECDSA의 경우)를 가져와 헤더에 지정된 알고리즘을 사용하여 서명함으로써 생성됩니다.
예를 들어 HMAC SHA256을 사용하는 경우 서명은 다음과 같이 생성됩니다:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
서명은 토큰이 변경되지 않았음을 보장합니다. 누군가 헤더나 페이로드에서 단 하나의 문자만 변경해도 서명 검증이 실패합니다.
완전한 JWT 예제
세 부분을 모두 합치면 다음과 같은 완전한 JWT를 얻게 됩니다:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT 디코더 도구를 사용하여 모든 JWT를 디코딩하고 검사하여 헤더, 페이로드를 확인하고 서명을 검증할 수 있습니다.
실제로 JWT 생성 및 활용하기
적절한 라이브러리를 사용하면 JWT 생성이 간단합니다. 다양한 프로그래밍 언어와 시나리오에서 JWT를 생성하고 사용하는 방법을 살펴보겠습니다.
Node.js에서 JWT 생성하기
jsonwebtoken 라이브러리는 Node.js 애플리케이션에서 가장 인기 있는 선택입니다:
const jwt = require('jsonwebtoken');
// 토큰 생성
const payload = {
sub: user.id,
email: user.email,
role: user.role
};
const secret = process.env.JWT_SECRET;
const options = {
expiresIn: '1h',
issuer: 'myapp.com'
};
const token = jwt.sign(payload, secret, options);
// 토큰 검증
try {
const decoded = jwt.verify(token, secret);
console.log(decoded);
} catch (error) {
console.error('유효하지 않은 토큰:', error.message);
}
Python에서 JWT 생성하기
Python 개발자는 일반적으로 PyJWT 라이브러리를 사용합니다:
import jwt
import datetime
# 토큰 생성
payload = {
'sub': user.id,
'email': user.email,
'role': user.role,
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),
'iat': datetime.datetime.utcnow()
}
secret = os.environ.get('JWT_SECRET')
token = jwt.encode(payload, secret, algorithm='HS256')
# 토큰 검증
try:
decoded = jwt.decode(token, secret, algorithms=['HS256'])
print(decoded)
except jwt.ExpiredSignatureError:
print('토큰이 만료되었습니다')
except jwt.InvalidTokenError:
print('유효하지 않은 토큰')
Java에서 JWT 생성하기
Java 애플리케이션의 경우 Auth0의 java-jwt 라이브러리가 널리 사용됩니다:
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
// 토큰 생성
Algorithm algorithm = Algorithm.HMAC256(secret);
String token = JWT.create()
.withSubject(user.getId())
.withClaim("email", user.getEmail())
.withClaim("role", user.getRole())
.withExpiresAt(new Date(System.currentTimeMillis() + 3600000))
.withIssuer("myapp.com")
.sign(algorithm);
// 토큰 검증
try {
DecodedJWT jwt = JWT.require(algorithm)
.withIssuer("myapp.com")
.build()
.verify(token);
} catch (Exception e) {
System.out.println("유효하지 않은 토큰: " + e.getMessage());
}
토큰 생성 모범 사례
JWT를 생성할 때 보안과 신뢰성을 보장하기 위해 다음 지침을 따르세요:
- 강력한 비밀 사용: JWT 비밀은 최소 256비트(32자)의 무작위 데이터여야 합니다. 소스 코드에 비밀을 하드코딩하지 마세요.
- 적절한 만료 시간 설정: 액세스 토큰은 수명이 짧아야 하며(15분~1시간), 리프레시 토큰은 더 오래 지속될 수 있습니다(며칠~몇 주).
- 필요한 클레임만 포함: 토큰 크기를 줄이고 정보 노출을 최소화하기 위해 페이로드를 간결하게 유지하세요.
- 올바른 알고리즘 사용: 대부분의 애플리케이션에서는 HS256이 충분합니다. 비밀을 공유하지 않고 토큰을 검증해야 하는 경우(마이크로서비스 등) RS256을 사용하세요.
전문가 팁: 리프레시 토큰을 사용하여 토큰 갱신 메커니즘을 구현하는 것을 고려하세요. 이를 통해 보안을 위해 액세스 토큰을 짧게 유지하면서 빈번한 재인증 없이 좋은 사용자 경험을 유지할 수 있습니다.
JWT 인증 워크플로우 설명
JWT가 애플리케이션을 통해 어떻게 흐르는지 이해하는 것은 올바르게 구현하는 데 중요합니다. 일반적인 인증 워크플로우를 단계별로 살펴보겠습니다.
초기 인증
- 사용자가 자격 증명 제출: 사용자가 인증 엔드포인트(일반적으로
POST /api/auth/login)에 사용자 이름과 비밀번호를 보냅니다. - 서버가 자격 증명 검증: 서버가 데이터베이스에 대해 자격 증명을 확인하고 비밀번호 해시가 일치하는지 확인합니다.
- 서버가 JWT 생성: 검증에 성공하면 서버는 사용자의 신원과 관련 클레임을 포함하는 JWT를 생성합니다.
- 서버가 토큰 반환: JWT가 클라이언트로 다시 전송되며, 일반적으로 응답 본문에 포함됩니다. 일부 구현에서는 HTTP 전용 쿠키로도 설정합니다.
후속 요청
- 클라이언트가 토큰 포함: 각 후속 요청에 대해 클라이언트는 Authorization 헤더에 JWT를 포함합니다:
Authorization: Bearer <token> - 서버가 토큰 검증: 서버가 토큰을 추출하고 서명을 확인하며 만료를 확인합니다.
- 서버가 요청 처리: 토큰이 유효하면 서버는 페이로드에서 사용자 정보를 추출하고 그에 따라 요청을 처리합니다.
- 서버가 응답 반환: 요청된 데이터 또는 작업 결과가 클라이언트로 반환됩니다.
토큰 갱신 플로우
액세스 토큰이 만료되면 갱신 플로우가 시작됩니다:
- 클라이언트가 만료 감지: 클라이언트가 401 Unauthorized 응답을 받거나 토큰의
exp클레임을 확인합니다. - 클라이언트가 리프레시 토큰 전송: 클라이언트가 전용 갱신 엔드포인트(
POST /api/auth/refresh)에 리프레시 토큰을 보냅니다. - 서버가 리프레시 토큰 검증: 서버가 리프레시 토큰을 확인하고 취소되었는지 확인합니다.
- 서버가 새 액세스 토큰 발급: 새 액세스 토큰이 생성되어 클라이언트로 반환됩니다.
- 클라이언트가 원래 요청 재시도: 클라이언트가 새 액세스 토큰을 사용하여 실패한 요청을 재시도합니다.
로그아웃 플로우
JWT를 사용한 로그아웃은 토큰이 무상태이므로 특별한 고려가 필요합니다:
- 클라이언트가 로그아웃 시작: 사용자가 로그아웃을 클릭하여
POST /api/auth/logout에 대한 요청을 트리거합니다. - 서버가 리프레시 토큰 무효화: 서버가 리프레시 토큰을 취소 목록에 추가하거나 데이터베이스에서 삭제합니다.
- 클라이언트가 토큰 폐기: 클라이언트가 스토리지에서 액세스 토큰과 리프레시 토큰을 모두 제거합니다.
- 클라이언트가 리디렉션: 사용자가 로그인 페이지 또는 공개 영역으로 리디렉션됩니다.
| 토큰 유형 | 일반적인 수명 | 저장 위치 | 목적 |
|---|---|---|---|
| 액세스 토큰 | 15분 - 1시간 | 메모리 또는 sessionStorage | API 요청 권한 부여 |
| 리프레시 토큰 | 7일 - 30일 | HTTP 전용 쿠키 또는 보안 저장소 | 새 액세스 토큰 획득 |
| ID 토큰 | 액세스 토큰과 동일 | 메모리 | 사용자 신원 정보 (OpenID Connect) |
빠른 팁: 클라이언트 애플리케이션에서 자동 토큰 갱신을 구현하여 토큰 만료를 원활하게 처리하세요. 이렇게 하면 사용자가 예기치 않게 로그아웃되는 것을 방지할 수 있습니다.