JWT 완벽 가이드 2026 - 구조, 보안, 모범 사례 총정리
JSON Web Token의 구조와 작동 원리부터 보안 취약점과 모범 사례까지. 실전 코드 예제와 함께 JWT 인증을 마스터합니다.
Toolypet Team
Development Team
JWT 완벽 가이드 2026
"JWT? 그냥 토큰 아니야?"
맞습니다. 하지만 그 "그냥 토큰"이 제대로 구현되지 않으면 심각한 보안 문제가 됩니다. 2026년 기준, 48%의 AI 생성 인증 코드에 보안 취약점이 존재한다는 연구 결과도 있습니다.
이 가이드에서는 JWT의 구조부터 실전 보안 모범 사례까지 완벽하게 다룹니다.
JWT란?
JWT(JSON Web Token)는 두 주체 간에 정보를 안전하게 전달하기 위한 컴팩트하고 자체 포함적인 방식입니다.
주요 특징
| 특징 | 설명 |
|---|---|
| Stateless | 서버에 세션 저장 불필요 |
| Self-contained | 필요한 정보가 토큰 내에 포함 |
| Compact | URL, HTTP 헤더에 사용 가능 |
| Signed | 변조 감지 가능 |
사용 사례
- API 인증: Bearer 토큰으로 API 요청
- SSO (Single Sign-On): 여러 서비스 간 인증 공유
- 정보 교환: 서명된 데이터 전달
- Authorization: 권한/역할 정보 포함
JWT 구조
JWT는 .(점)으로 구분된 3개의 Base64URL 인코딩된 부분으로 구성됩니다.
xxxxx.yyyyy.zzzzz
| | |
Header Payload Signature
1. Header
{
"alg": "HS256",
"typ": "JWT"
}
| 필드 | 설명 |
|---|---|
alg | 서명 알고리즘 (HS256, RS256 등) |
typ | 토큰 타입 (JWT) |
2. Payload (Claims)
{
"sub": "user123",
"name": "홍길동",
"email": "hong@example.com",
"role": "admin",
"iat": 1708502400,
"exp": 1708588800
}
등록된 클레임 (Registered Claims)
| 클레임 | 설명 | 예시 |
|---|---|---|
iss | 발급자 (Issuer) | "https://auth.example.com" |
sub | 주체 (Subject) | "user123" |
aud | 대상자 (Audience) | "https://api.example.com" |
exp | 만료 시간 (Expiration) | 1708588800 |
nbf | 활성화 시간 (Not Before) | 1708502400 |
iat | 발급 시간 (Issued At) | 1708502400 |
jti | JWT 고유 ID | "abc123" |
공개/비공개 클레임
{
"role": "admin", // 비공개 클레임
"email": "user@example.com",
"permissions": ["read", "write"]
}
3. Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
서명 알고리즘
대칭키 (Symmetric)
| 알고리즘 | 설명 | 사용 사례 |
|---|---|---|
| HS256 | HMAC + SHA-256 | 단일 서버, 간단한 구현 |
| HS384 | HMAC + SHA-384 | 더 강한 보안 |
| HS512 | HMAC + SHA-512 | 최고 수준 보안 |
// Node.js - HS256
const jwt = require('jsonwebtoken');
const token = jwt.sign(
{ userId: 123, role: 'admin' },
'your-256-bit-secret', // 동일한 시크릿으로 검증
{ algorithm: 'HS256', expiresIn: '1h' }
);
비대칭키 (Asymmetric)
| 알고리즘 | 설명 | 사용 사례 |
|---|---|---|
| RS256 | RSA + SHA-256 | 마이크로서비스, 공개 검증 |
| RS384 | RSA + SHA-384 | 높은 보안 요구 |
| RS512 | RSA + SHA-512 | 최고 수준 보안 |
| ES256 | ECDSA + P-256 | 짧은 키, 모바일 |
// RS256 - 비대칭
const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');
// 발급 (Private Key)
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
// 검증 (Public Key)
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
알고리즘 선택 기준
| 상황 | 권장 알고리즘 |
|---|---|
| 단일 서버 | HS256 |
| 마이크로서비스 | RS256 |
| 공개 검증 필요 | RS256 / ES256 |
| 모바일/IoT | ES256 (짧은 키) |
실전 구현
Node.js (Express)
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const SECRET = process.env.JWT_SECRET; // 환경변수에서 로드
// 토큰 발급
app.post('/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' });
}
const token = jwt.sign(
{
sub: user.id,
email: user.email,
role: user.role,
},
SECRET,
{
expiresIn: '1h',
issuer: 'your-app-name',
}
);
res.json({ token });
});
// 미들웨어로 검증
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, SECRET, {
issuer: 'your-app-name',
algorithms: ['HS256'],
});
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
};
// 보호된 라우트
app.get('/protected', authMiddleware, (req, res) => {
res.json({ message: `Hello, ${req.user.email}` });
});
Python (FastAPI)
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from datetime import datetime, timedelta
import os
app = FastAPI()
security = HTTPBearer()
SECRET = os.environ.get("JWT_SECRET")
# 토큰 발급
def create_token(user_id: str, role: str) -> str:
payload = {
"sub": user_id,
"role": role,
"iat": datetime.utcnow(),
"exp": datetime.utcnow() + timedelta(hours=1),
}
return jwt.encode(payload, SECRET, algorithm="HS256")
# 토큰 검증
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
token = credentials.credentials
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
# 보호된 엔드포인트
@app.get("/protected")
def protected_route(user: dict = Depends(verify_token)):
return {"message": f"Hello, {user['sub']}"}
보안 취약점과 대응
1. Algorithm None 공격
// 악의적인 헤더
{
"alg": "none",
"typ": "JWT"
}
대응:
// 허용 알고리즘 명시
jwt.verify(token, secret, { algorithms: ['HS256'] }); // ✅
jwt.verify(token, secret); // ❌ 위험
2. 시크릿 키 약함
// ❌ 위험
const SECRET = 'secret123';
// ✅ 안전 (최소 256비트)
const SECRET = crypto.randomBytes(32).toString('hex');
3. 민감 정보 노출
// ❌ 위험: Payload는 Base64 디코딩 가능
{
"password": "user_password", // 절대 금지!
"creditCard": "1234-5678-9012-3456"
}
// ✅ 안전
{
"sub": "user123",
"role": "admin"
}
4. 토큰 탈취
대응 전략:
- 짧은 만료 시간 (15분 ~ 1시간)
- Refresh Token 사용
- HTTPS 필수
- HttpOnly 쿠키 (XSS 방지)
5. JWT 재사용 (Replay Attack)
// jti (JWT ID) 사용
const token = jwt.sign({
sub: user.id,
jti: crypto.randomUUID(), // 고유 ID
}, SECRET);
// 서버에서 jti 블랙리스트 관리
Access Token + Refresh Token
왜 필요한가?
| 토큰 | 수명 | 용도 |
|---|---|---|
| Access Token | 15분 ~ 1시간 | API 인증 |
| Refresh Token | 7일 ~ 30일 | Access Token 갱신 |
구현 예시
// 로그인 시 두 토큰 발급
app.post('/login', async (req, res) => {
const user = await verifyCredentials(req.body);
const accessToken = jwt.sign(
{ sub: user.id, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
// Refresh Token은 HttpOnly 쿠키로
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken });
});
// 토큰 갱신
app.post('/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
const newAccessToken = jwt.sign(
{ sub: decoded.sub, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch (err) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
JWT 디버깅
토큰 디코딩
JWT Decoder에서 토큰을 붙여넣으면:
- Header 확인
- Payload 확인
- 만료 시간 확인
- 서명 검증 (시크릿 입력 시)
CLI로 디코딩
# Header 디코딩
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
# Payload 디코딩
echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ" | base64 -d
모범 사례 체크리스트
발급
- 강력한 시크릿 키 (256비트 이상)
- 알고리즘 명시 (HS256, RS256)
- 짧은 만료 시간 (15분 ~ 1시간)
- 필수 클레임 포함 (sub, iat, exp, iss)
- 민감 정보 미포함
검증
- 알고리즘 화이트리스트 (
algorithms: ['HS256']) - 만료 시간 검증
- 발급자(iss) 검증
- 대상자(aud) 검증
저장 및 전송
- HTTPS 필수
- HttpOnly 쿠키 (XSS 방지)
- SameSite 속성 (CSRF 방지)
- localStorage 지양 (XSS 취약)
FAQ
Q1: JWT vs 세션, 언제 무엇을?
A:
- JWT: Stateless API, 마이크로서비스, 모바일 앱
- 세션: 전통적 웹앱, 서버 사이드 렌더링, 즉시 로그아웃 필요
Q2: JWT가 탈취되면?
A: Access Token 탈취 시 만료까지 유효합니다. 따라서:
- 짧은 만료 시간 설정 (15분)
- Refresh Token Rotation
- 의심 시 모든 토큰 무효화
Q3: 왜 localStorage에 저장하면 안 되나요?
A: XSS 공격에 취약합니다. JavaScript로 접근 가능하므로 악성 스크립트가 토큰을 탈취할 수 있습니다. HttpOnly 쿠키가 더 안전합니다.
Q4: RS256이 HS256보다 안전한가요?
A: "더 안전"하다기보다 "다른 용도"입니다.
- HS256: 동일한 시크릿 공유, 단일 서버
- RS256: 공개키로 검증, 다중 서비스
Q5: 토큰 블랙리스트가 필요한가요?
A: 즉시 로그아웃이 필요하면 필요합니다. 하지만 stateless의 장점이 줄어듭니다. 짧은 만료 시간 + Refresh Token이 대안입니다.
마무리
JWT 보안의 핵심:
- 강력한 시크릿: 256비트 이상 랜덤
- 알고리즘 검증: 화이트리스트 방식
- 짧은 수명: Access 15분, Refresh 7일
- 안전한 저장: HttpOnly + Secure 쿠키
- 민감 정보 제외: Payload는 누구나 디코딩 가능
관련 도구
| 도구 | 용도 |
|---|---|
| JWT Decoder | JWT 디코딩 및 검증 |
| Base64 Encoder | Base64 인코딩/디코딩 |
| Hash Generator | 해시 생성 |
저자 소개
Toolypet Team
Development Team
The Toolypet Team creates free, privacy-focused web tools for developers and designers. All tools run entirely in your browser with no data sent to servers.