JWT 토큰 설명: 현대 웹 앱을 위한 인증
· 12분 읽기
목차
JSON Web Token(JWT)은 현대 웹 애플리케이션에서 인증의 사실상 표준이 되었습니다. 두 당사자 간에 클레임을 표현하는 컴팩트하고 URL 안전한 방법을 제공하여, 공유 세션 저장소 없이 수평적으로 확장 가능한 무상태 인증을 가능하게 합니다.
REST API, 마이크로서비스 아키텍처 또는 단일 페이지 애플리케이션을 구축하든, JWT를 이해하는 것은 안전하고 확장 가능한 인증을 구현하는 데 필수적입니다. 이 종합 가이드는 JWT의 작동 방식, 구조, 보안 고려사항 및 프로덕션 준비 모범 사례를 설명합니다.
JWT란 무엇인가?
JWT("jot"으로 발음)는 JSON 객체로 당사자 간에 정보를 안전하게 전송하기 위한 컴팩트하고 자체 포함된 형식을 정의하는 공개 표준(RFC 7519)입니다. 정보는 비밀 키(HMAC) 또는 공개/개인 키 쌍(RSA 또는 ECDSA)을 사용하여 디지털 서명되어 토큰의 무결성과 진위성을 보장합니다.
서버가 메모리나 데이터베이스에 세션 상태를 유지하는 전통적인 세션 기반 인증과 달리, JWT는 무상태입니다. 사용자의 신원을 확인하는 데 필요한 모든 정보가 토큰 자체에 포함되어 있습니다.
JWT의 주요 사용 사례
- 인증: 사용자가 로그인한 후 서버는 JWT를 발급하고 클라이언트는 후속 요청에 이를 포함하여 신원을 증명합니다
- 권한 부여: JWT는 사용자 역할, 권한 및 범위를 전달할 수 있어 서버가 데이터베이스 조회 없이 세밀한 액세스 제어 결정을 내릴 수 있습니다
- 정보 교환: JWT의 서명된 특성은 발신자가 주장하는 사람이 맞고 전송 중에 데이터가 변조되지 않았음을 보장합니다
- Single Sign-On (SSO): JWT는 여러 도메인과 서비스에서 원활한 인증을 가능하게 합니다
- API 인증: 모바일 앱과 타사 통합은 서버 측 세션을 유지하지 않고 API 요청을 인증할 수 있습니다
프로 팁: 개발 중에 토큰을 검사하고 검증하려면 JWT 디코더를 사용하세요. 구조를 이해하고 프로덕션에 도달하기 전에 일반적인 문제를 파악하는 데 도움이 됩니다.
JWT 구조 분석
JWT는 점(.)으로 구분된 세 가지 고유한 부분으로 구성됩니다: 헤더.페이로드.서명
완전한 JWT는 다음과 같습니다:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRGV2ZWxvcGVyIiwiZW1haWwiOiJqYW5lQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzEwNTkwNDAwLCJleHAiOjE3MTA2NzY4MDB9.4Adcj0mYZ8s5vxjKvV8pF7jKX9s8vZ5xJ3kL9mN2pQ4
파트 1: 헤더
헤더는 일반적으로 토큰의 암호화 작업을 설명하는 두 개의 필드를 포함합니다:
{
"alg": "HS256",
"typ": "JWT"
}
alg는 서명 알고리즘을 지정합니다(HS256, RS256, ES256 등)typ는 토큰 유형을 선언하며 항상 "JWT"입니다
이 JSON 객체는 Base64Url로 인코딩되어 JWT의 첫 번째 부분을 형성합니다. 인코딩은 HTTP 헤더에서 전송하기 위해 URL 안전하고 컴팩트하게 만듭니다.
파트 2: 페이로드
페이로드는 클레임(사용자 및 추가 메타데이터에 대한 진술)을 포함합니다. 클레임은 세 가지 유형으로 분류됩니다:
{
"sub": "1234567890",
"name": "Jane Developer",
"email": "[email protected]",
"role": "admin",
"iat": 1710590400,
"exp": 1710676800
}
등록된 클레임은 JWT 사양에 의해 미리 정의되며 표준 정보를 제공합니다:
iss(발급자): 토큰을 발급한 사람을 식별합니다sub(주체): 토큰의 주체를 식별합니다(일반적으로 사용자 ID)aud(대상): 의도된 수신자를 식별합니다exp(만료 시간): 토큰이 만료되는 Unix 타임스탬프nbf(이전 불가): 이 시간 이전에는 토큰이 유효하지 않습니다iat(발급 시간): 토큰이 생성된 시간jti(JWT ID): 토큰의 고유 식별자
공개 클레임은 IANA JSON Web Token 레지스트리에 정의되거나 충돌 방지 이름(예: URL)을 사용해야 하는 사용자 정의 클레임입니다.
비공개 클레임은 role, permissions 또는 organizationId와 같이 당사자 간에 합의된 사용자 정의 클레임입니다.
중요: 페이로드는 Base64Url로 인코딩될 뿐 암호화되지 않습니다. 비밀번호, 신용카드 번호 또는 주민등록번호와 같은 민감한 정보를 JWT 페이로드에 절대 저장하지 마세요.
파트 3: 서명
서명은 토큰이 변경되지 않았음을 보장합니다. 인코딩된 헤더, 인코딩된 페이로드, 비밀 키를 가져와 헤더에 지정된 알고리즘을 적용하여 생성됩니다:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
서명을 통해 수신자는 발신자가 주장하는 사람이 맞고 메시지가 도중에 변경되지 않았음을 확인할 수 있습니다.
서명 알고리즘 비교
| 알고리즘 | 유형 | 보안 수준 | 사용 사례 | 키 관리 |
|---|---|---|---|---|
| HS256 | 대칭(HMAC) | 좋음 | 단일 서비스, 내부 API | 공유 비밀 |
| RS256 | 비대칭(RSA) | 매우 좋음 | 다중 서비스, 공개 API | 공개/개인 키 쌍 |
| ES256 | 비대칭(ECDSA) | 우수 | 높은 보안 요구사항 | 공개/개인 키 쌍 |
| PS256 | 비대칭(RSA-PSS) | 우수 | 현대적인 애플리케이션 | 공개/개인 키 쌍 |
JWT 인증 작동 방식
전체 인증 흐름을 이해하면 JWT를 올바르게 구현하고 문제를 효과적으로 해결하는 데 도움이 됩니다.
전체 인증 흐름
- 사용자 로그인: 클라이언트가 인증 엔드포인트로 자격 증명(사용자 이름/비밀번호)을 보냅니다
- 자격 증명 확인: 서버가 데이터베이스에 대해 자격 증명을 검증합니다
- 토큰 생성: 인증 성공 시 서버는 사용자 정보와 클레임을 포함하는 JWT를 생성합니다
- 토큰 전달: 서버가 JWT를 클라이언트에 다시 보냅니다(일반적으로 응답 본문에)
- 토큰 저장: 클라이언트가 JWT를 저장합니다(메모리, localStorage 또는 httpOnly 쿠키에)
- 인증된 요청: 후속 요청에 대해 클라이언트는 Authorization 헤더에 JWT를 포함합니다
- 토큰 검증: 서버가 JWT 서명을 검증하고 만료를 확인합니다
- 액세스 허용: 유효한 경우 서버는 토큰의 클레임을 사용하여 요청을 처리합니다
인증 요청 예시
POST /api/auth/login HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"email": "[email protected]",
"password": "securePassword123"
}
인증 응답 예시
HTTP/1.1 200 OK
Content-Type: application/json
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 3600,
"tokenType": "Bearer"
}
인증된 API 요청 예시
GET /api/users/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT vs 세션 기반 인증
JWT와 세션 기반 인증 중 선택하는 것은 애플리케이션의 아키텍처, 규모 및 요구사항에 따라 다릅니다.
| 측면 | JWT | 세션 기반 |
|---|---|---|
| 상태 | 무상태(서버가 토큰을 저장하지 않음) | 상태 유지(서버가 세션 데이터를 저장) |
| 확장성 | 우수(공유 저장소 불필요) | 세션 저장소 필요(Redis, 데이터베이스) |
| 취소 | 어려움(블랙리스트 또는 짧은 만료 필요) | 쉬움(저장소에서 세션 삭제) |
| 크기 | 더 큼(모든 요청과 함께 전송) | 더 작음(세션 ID만) |
| 크로스 도메인 | 쉬움(CORS 친화적) | 복잡함(쿠키 도메인 제한) |
| 모바일 앱 | 이상적(쿠키 지원 불필요) | 어려움(쿠키 처리) |
| 보안 | 신중한 구현 필요 | 잘 확립된 패턴 |
JWT를 사용해야 하는 경우
- 인증을 공유해야 하는 마이크로서비스 구축
- 모바일 애플리케이션 또는 SPA 개발
- 타사 통합을 위한 공개 API 생성
- 여러 도메인에서 Single Sign-On(SSO) 구현
- 공유 세션 저장소 없이 수평적으로 확장
세션을 사용해야 하는 경우
- 전통적인 서버 렌더링 웹 애플리케이션 구축
- 즉각적인 토큰 취소 기능 필요
- 서버 측 제어가 필요한 민감한 데이터 작업
- 대역폭 최소화(세션은 더 작은 쿠키 사용)
- 복잡한 세션 관리 기능 구현
애플리케이션에 JWT 구현하기
인기 있는 프로그래밍 언어와 프레임워크의 실용적인 구현 예제를 살펴보겠습니다.
Express를 사용한 Node.js
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();
const SECRET_KEY = process.env.JWT_SECRET;
// 로그인 엔드포인트
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
// 자격 증명 확인(의사 코드)
const user = await verifyCredentials(email, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// JWT 생성
const token = jwt.sign(
{
sub: user.id,
email: user.email,
role: user.role
},
SECRET_KEY,
{ expiresIn: '1h' }
);
res.json({ accessToken: token });
});
// 인증 미들웨어
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user;
next();
});
};
// 보호된 라우트
app.get('/api/users/profile', authenticateToken, (req, res) => {
res.json({ user: req.user });
});
Flask를 사용한 Python
from flask import Flask, request, jsonify
import jwt
import datetime
import os
app = Flask(__name__)
SECRET_KEY = os.getenv('JWT_SECRET')
@app.route('/api/auth/login', methods=['POST'])
def login():
data = request.get_json()
email = data.get('email')
password = data.get('password')
# 자격 증명 확인
user = verify_credentials(email, password)
if not user:
return jsonify({'error': 'Invalid credentials'}), 401
# JWT 생성
token = jwt.encode({
'sub': user['id'],
'email': user['email'],
'role': user['role'],
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}, SECRET_KEY, algorithm='HS256')
return jsonify({'accessToken': token})
def token_required(f):
def decorator(*args, **kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({'error': 'No token provided'}), 401
try:
token = token.split(' ')[1]
data = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
request.user = data
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token expired'}), 403
except jwt.InvalidTokenError:
return jsonify({'error': 'Invalid token'}), 403
return f(*args, **kwargs)
decorator.__name__ = f.__name__
return decorator
@app.route('/api/users/profile')
@token_required
def profile():
return jsonify({'user': request.user})
빠른 팁: 비밀 키에는 항상 환경 변수를 사용하세요. 소스 코드에 하드코딩하거나 버전 관리에 커밋하지 마세요. 강력한 비밀을 생성하려면 비밀번호 생성기와 같은 도구를 사용하세요.