JWT 완벽 가이드 2025 - 토큰 기반 인증의 모든 것
현대 웹 애플리케이션에서 JWT(JSON Web Token)는 사용자 인증의 표준으로 자리잡았습니다. 이 가이드에서는 JWT의 기본 개념부터 실무 활용, 보안 고려사항까지 모든 것을 다룹니다.
JWT란 무엇인가?
JWT(JSON Web Token)는 당사자 간에 정보를 안전하게 전송하기 위한 컴팩트하고 자체 포함된(self-contained) 방식의 토큰입니다. JSON 객체로 정보를 안전하게 전달할 수 있으며, 디지털 서명이 되어 있어 검증과 신뢰가 가능합니다.
JWT의 주요 특징
- 자체 포함(Self-contained): 토큰 자체에 사용자 정보가 포함
- 확장 가능(Extensible): 필요한 정보를 자유롭게 추가 가능
- 무상태(Stateless): 서버에 세션 저장 불필요
- 확장성: 여러 서버 간 정보 공유 용이
JWT의 구조
JWT는 점(.)으로 구분된 세 부분으로 구성됩니다:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1. Header (헤더)
토큰의 타입과 서명 알고리즘 정보를 포함합니다.
{
"alg": "HS256",
"typ": "JWT"
}
주요 필드:
alg: 서명 알고리즘 (HS256, RS256 등)typ: 토큰 타입 (JWT)
2. Payload (페이로드)
실제 전달할 정보(클레임)를 포함합니다.
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}
표준 클레임 (Registered Claims):
| 클레임 | 이름 | 설명 |
iss | Issuer | 토큰 발급자 |
sub | Subject | 토큰 주제 (사용자 ID) |
aud | Audience | 토큰 대상자 |
exp | Expiration Time | 만료 시간 (UNIX timestamp) |
nbf | Not Before | 토큰 활성화 시간 |
iat | Issued At | 토큰 발급 시간 |
jti | JWT ID | 토큰 고유 식별자 |
커스텀 클레임:
{
"userId": "12345",
"email": "user@example.com",
"role": "admin",
"permissions": ["read", "write", "delete"]
}
3. Signature (서명)
토큰의 무결성을 검증하는 서명입니다.
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
JWT 생성 및 검증 과정
토큰 생성 (서버)
// Node.js 예시
const jwt = require('jsonwebtoken');
const payload = {
userId: '12345',
email: 'user@example.com',
role: 'admin'
};
const secret = process.env.JWT_SECRET;
const options = {
expiresIn: '1h', // 1시간 후 만료
issuer: 'my-app'
};
const token = jwt.sign(payload, secret, options);
토큰 검증 (서버)
try {
const decoded = jwt.verify(token, secret);
console.log(decoded);
// { userId: '12345', email: 'user@example.com', role: 'admin', ... }
} catch (error) {
// 토큰 검증 실패
console.error('Invalid token:', error.message);
}
클라이언트에서 사용
// 로그인 후 토큰 저장
localStorage.setItem('token', token);
// API 요청 시 토큰 포함
fetch('https://api.example.com/user', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
JWT 인증 플로우
1. 사용자 로그인
↓
2. 서버: 자격증명 검증
↓
3. 서버: JWT 생성 및 반환
↓
4. 클라이언트: 토큰 저장 (localStorage/sessionStorage)
↓
5. 이후 요청마다 토큰을 Authorization 헤더에 포함
↓
6. 서버: 토큰 검증 후 요청 처리
JWT의 장점
1. 확장성 (Scalability)
세션 방식: [서버1] ← 세션 공유 → [서버2]
↓ ↓
[세션 저장소] [세션 저장소]
JWT 방식: [서버1] [서버2]
↓ ↓
(검증만) (검증만)
2. 무상태성 (Stateless)
- 서버에 세션 정보 저장 불필요
- 메모리 사용량 감소
- 수평 확장 용이
3. 모바일 친화적
- 쿠키 의존성 없음
- 네이티브 앱에서도 동일한 방식 사용 가능
4. 마이크로서비스 아키텍처에 적합
- 서비스 간 인증 정보 공유 용이
- API 게이트웨이에서 토큰 검증
JWT의 단점 및 해결 방법
1. 토큰 크기
문제: 세션 ID보다 크기가 큼 (평균 200-300 bytes)
해결책:
- 필요한 클레임만 포함
- 민감한 정보는 제외
- 압축 알고리즘 사용 고려
2. 토큰 취소 불가
문제: 발급된 토큰은 만료 전까지 유효
해결책:
// Blacklist 방식
const blacklist = new Set();
function revokeToken(token) {
blacklist.add(token);
}
function verifyToken(token) {
if (blacklist.has(token)) {
throw new Error('Token has been revoked');
}
return jwt.verify(token, secret);
}
// Refresh Token 패턴
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
const refreshToken = jwt.sign(payload, refreshSecret, { expiresIn: '7d' });
3. XSS 공격 위험
문제: localStorage에 저장 시 XSS 공격에 취약
해결책:
// HttpOnly 쿠키에 저장
res.cookie('token', token, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict' // CSRF 방지
});
실무 예제: Express.js 미들웨어
const jwt = require('jsonwebtoken');
// JWT 인증 미들웨어
function authenticateToken(req, res, next) {
// Authorization 헤더에서 토큰 추출
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
// 토큰 검증
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(403).json({ error: 'Invalid token' });
}
}
// 보호된 라우트
app.get('/api/protected', authenticateToken, (req, res) => {
res.json({
message: 'Protected data',
user: req.user
});
});
// 로그인 라우트
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
// 사용자 검증 (실제로는 DB 조회)
const user = await User.findOne({ email });
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// JWT 생성
const token = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token });
});
Refresh Token 패턴
// Access Token: 짧은 만료 시간 (15분)
// Refresh Token: 긴 만료 시간 (7일)
app.post('/api/token/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
try {
// Refresh Token 검증
const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
// 새로운 Access Token 발급
const newAccessToken = jwt.sign(
{ userId: decoded.userId, email: decoded.email },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch (error) {
res.status(403).json({ error: 'Invalid refresh token' });
}
});
알고리즘 선택 가이드
HMAC (HS256, HS384, HS512)
- 대칭키 알고리즘: 같은 비밀키로 서명과 검증
- 장점: 빠름, 구현 간단
- 단점: 키 공유 필요
- 적합한 경우: 단일 서버 또는 신뢰할 수 있는 환경
const token = jwt.sign(payload, 'secret-key', { algorithm: 'HS256' });
RSA (RS256, RS384, RS512)
- 비대칭키 알고리즘: 개인키로 서명, 공개키로 검증
- 장점: 공개키 배포 가능, 보안성 높음
- 단점: 느림, 복잡
- 적합한 경우: 마이크로서비스, 제3자 검증 필요
const privateKey = fs.readFileSync('private.key');
const publicKey = fs.readFileSync('public.key');
// 서명
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
// 검증
jwt.verify(token, publicKey);
JWT 디버깅 도구
JWT 개발 시 다음 도구들이 유용합니다:
Fun Utils JWT Debugger
우리 사이트의 JWT Debugger에서 다음 기능을 제공합니다:
- 🔓 JWT 디코딩: 토큰을 Header, Payload, Signature로 분리
- ✅ 토큰 검증: 만료 시간, 발급 시간 등 자동 검증
- 📝 샘플 생성: 테스트용 JWT 토큰 즉시 생성
- ⏰ 시간 정보: 만료까지 남은 시간 실시간 표시
- 📋 Base64 복사: 디버깅을 위한 원본 형식 복사
사용법:
보안 체크리스트
✅ 필수 사항
- [ ] HTTPS 사용 (토큰 탈취 방지)
- [ ] 짧은 만료 시간 설정 (15-60분 권장)
- [ ] 비밀키 안전하게 관리 (환경 변수 사용)
- [ ] 알고리즘 명시 (
alg: none방지) - [ ] 민감한 정보 제외 (비밀번호, 카드번호 등)
⚠️ 권장 사항
- [ ] Refresh Token 패턴 사용
- [ ] Rate Limiting 적용
- [ ] 토큰 Blacklist 구현 (필요시)
- [ ] 로그아웃 시 토큰 무효화
- [ ] IP 주소 검증 (선택적)
❌ 하지 말아야 할 것
- ❌
alg: none사용 - ❌ 약한 비밀키 사용
- ❌ 토큰에 민감한 정보 포함
- ❌ HTTP로 토큰 전송
- ❌ 만료 시간 설정하지 않기
실전 팁
1. 환경별 만료 시간
const expiresIn = process.env.NODE_ENV === 'production'
? '15m' // 프로덕션: 15분
: '24h'; // 개발: 24시간
2. 에러 처리
try {
const decoded = jwt.verify(token, secret);
} catch (error) {
if (error.name === 'TokenExpiredError') {
// 토큰 만료 → Refresh Token으로 재발급
} else if (error.name === 'JsonWebTokenError') {
// 잘못된 토큰 → 재로그인 필요
} else if (error.name === 'NotBeforeError') {
// 토큰이 아직 활성화되지 않음
}
}
3. 클레임 검증
const decoded = jwt.verify(token, secret, {
issuer: 'my-app', // 발급자 확인
audience: 'my-frontend', // 대상 확인
clockTolerance: 10 // 시간 오차 허용 (초)
});
마이크로서비스에서의 JWT
[API Gateway]
↓ (JWT 검증)
├─→ [User Service] (토큰 재사용)
├─→ [Order Service] (토큰 재사용)
└─→ [Payment Service] (토큰 재사용)
장점:
- 각 서비스가 독립적으로 토큰 검증
- 중앙화된 인증 서버 불필요
- 네트워크 요청 감소
성능 최적화
1. 토큰 캐싱
const cache = new Map();
function verifyWithCache(token) {
if (cache.has(token)) {
return cache.get(token);
}
const decoded = jwt.verify(token, secret);
cache.set(token, decoded);
// 만료 시간 후 캐시 삭제
setTimeout(() => cache.delete(token), decoded.exp * 1000 - Date.now());
return decoded;
}
2. 비동기 검증
const { promisify } = require('util');
const verifyAsync = promisify(jwt.verify);
async function authenticateToken(req, res, next) {
try {
const token = req.headers.authorization?.split(' ')[1];
req.user = await verifyAsync(token, secret);
next();
} catch (error) {
res.status(401).json({ error: 'Unauthorized' });
}
}
결론
JWT는 현대 웹 애플리케이션의 인증 표준으로 자리잡았습니다. 올바르게 사용하면 확장 가능하고 안전한 인증 시스템을 구축할 수 있습니다.
핵심 요약:
- ✅ 무상태(Stateless) 인증으로 확장성 향상
- ✅ 마이크로서비스 아키텍처에 적합
- ⚠️ 보안 고려사항 반드시 준수
- ⚠️ Refresh Token 패턴으로 보안 강화
- 🔧 Fun Utils JWT Debugger로 쉽게 디버깅
관련 도구:
더 알아보기: