JSON Web Tokens (JWT) 완벽 가이드: 인증의 모든 것
JWT의 구조, 작동 원리, 보안 모범 사례를 배워보세요. 실용적인 예제와 함께 인증 시스템 구축 방법을 완벽 정리.
Big JSON Team
• Technical WriterExpert in JSON data manipulation, API development, and web technologies. Passionate about creating tools that make developers' lives easier.
# JSON Web Tokens (JWT) 완벽 가이드
JWT는 현대 웹 애플리케이션에서 가장 널리 사용되는 인증 방식입니다. 구조부터 보안까지 모든 것을 배워보세요.
JWT란?
JWT (JSON Web Token)는 당사자 간에 정보를 안전하게 전송하기 위한 컴팩트하고 자체 포함된 방식입니다.
특징
- 자체 포함: 필요한 모든 정보를 포함
- 디지털 서명: 변조 방지
- URL 안전: Base64 URL 인코딩
- 무상태: 서버에 세션 저장 불필요
사용 사례
JWT 구조
JWT는 세 부분으로 구성:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik1pa2UiLCJpYXQiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
구조:
Header.Payload.Signature
1. Header (헤더)
토큰 타입과 알고리즘:
{
"alg": "HS256",
"typ": "JWT"
}
인코딩:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2. Payload (페이로드)
클레임 (정보):
{
"sub": "1234567890",
"name": "홍길동",
"iat": 1516239022,
"exp": 1516242622
}
인코딩:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik1pa2UiLCJpYXQiOjE1MTYyMzkwMjJ9
3. Signature (서명)
변조 방지 서명:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
클레임 (Claims)
Registered Claims (등록된 클레임)
표준 클레임:
iss(Issuer): 발급자sub(Subject): 주제 (사용자 ID)aud(Audience): 대상exp(Expiration): 만료 시간nbf(Not Before): 유효 시작 시간iat(Issued At): 발급 시간jti(JWT ID): 고유 식별자
Public Claims (공개 클레임)
충돌 방지를 위해 URI 형식:
{
"https://example.com/is_admin": true
}
Private Claims (비공개 클레임)
당사자 간 합의된 클레임:
{
"name": "홍길동",
"role": "admin",
"department": "개발팀"
}
Node.js 구현
jsonwebtoken 라이브러리
npm install jsonwebtoken
토큰 생성
const jwt = require('jsonwebtoken');
const secret = 'your-secret-key';
// 토큰 생성
const token = jwt.sign(
{
sub: '12345',
name: '홍길동',
role: 'admin'
},
secret,
{
expiresIn: '1h',
issuer: 'myapp'
}
);
console.log('토큰:', token);
토큰 검증
const jwt = require('jsonwebtoken');
const secret = 'your-secret-key';
try {
const decoded = jwt.verify(token, secret);
console.log('유효한 토큰:', decoded);
/
{
sub: '12345',
name: '홍길동',
role: 'admin',
iat: 1642348800,
exp: 1642352400,
iss: 'myapp'
}
/
} catch (error) {
if (error.name === 'TokenExpiredError') {
console.log('토큰 만료');
} else if (error.name === 'JsonWebTokenError') {
console.log('유효하지 않은 토큰');
} else {
console.log('검증 오류:', error.message);
}
}
디코딩 (검증 없이)
const jwt = require('jsonwebtoken');
// 서명 검증 없이 디코딩
const decoded = jwt.decode(token, { complete: true });
console.log('헤더:', decoded.header);
console.log('페이로드:', decoded.payload);
Express 미들웨어
인증 미들웨어
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: '토큰이 없습니다' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: '유효하지 않은 토큰' });
}
req.user = user;
next();
});
}
// 사용
app.get('/api/protected', authenticateToken, (req, res) => {
res.json({
message: '보호된 라우트',
user: req.user
});
});
역할 기반 접근 제어
function requireRole(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '인증 필요' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: '권한 없음' });
}
next();
};
}
// 사용
app.delete(
'/api/users/:id',
authenticateToken,
requireRole('admin', 'moderator'),
(req, res) => {
// 관리자와 모더레이터만 접근 가능
}
);
로그인 구현
완전한 로그인 시스템
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());
const JWT_SECRET = process.env.JWT_SECRET;
const REFRESH_SECRET = process.env.REFRESH_SECRET;
// 사용자 DB (예시)
const users = [
{
id: 1,
username: 'hong',
password: '$2b$10$...', // 해시된 비밀번호
role: 'admin'
}
];
// 로그인
app.post('/api/auth/login', async (req, res) => {
const { username, password } = req.body;
// 사용자 찾기
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({ error: '사용자를 찾을 수 없습니다' });
}
// 비밀번호 검증
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
return res.status(401).json({ error: '비밀번호가 틀립니다' });
}
// Access Token 생성
const accessToken = jwt.sign(
{
sub: user.id,
username: user.username,
role: user.role
},
JWT_SECRET,
{ expiresIn: '15m' }
);
// Refresh Token 생성
const refreshToken = jwt.sign(
{ sub: user.id },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
res.json({
accessToken,
refreshToken,
user: {
id: user.id,
username: user.username,
role: user.role
}
});
});
// 토큰 갱신
app.post('/api/auth/refresh', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh Token 필요' });
}
try {
const payload = jwt.verify(refreshToken, REFRESH_SECRET);
// 새 Access Token 생성
const accessToken = jwt.sign(
{
sub: payload.sub,
username: payload.username,
role: payload.role
},
JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken });
} catch (error) {
res.status(403).json({ error: '유효하지 않은 Refresh Token' });
}
});
// 로그아웃 (클라이언트에서 토큰 삭제)
app.post('/api/auth/logout', (req, res) => {
// Refresh Token을 블랙리스트에 추가 (선택)
res.json({ message: '로그아웃 성공' });
});
클라이언트 사용
Fetch API
// 로그인
async function login(username, password) {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
// 토큰 저장
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);
return data;
}
// API 요청
async function fetchProtectedData() {
const token = localStorage.getItem('accessToken');
const response = await fetch('/api/protected', {
headers: {
'Authorization': Bearer ${token}
}
});
if (response.status === 401) {
// 토큰 만료, 갱신 시도
await refreshAccessToken();
return fetchProtectedData(); // 재시도
}
return response.json();
}
// 토큰 갱신
async function refreshAccessToken() {
const refreshToken = localStorage.getItem('refreshToken');
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refreshToken })
});
const data = await response.json();
localStorage.setItem('accessToken', data.accessToken);
}
Axios Interceptor
import axios from 'axios';
const api = axios.create({
baseURL: '/api'
});
// 요청 인터셉터
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = Bearer ${token};
}
return config;
},
(error) => Promise.reject(error)
);
// 응답 인터셉터
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('/api/auth/refresh', {
refreshToken
});
localStorage.setItem('accessToken', response.data.accessToken);
originalRequest.headers.Authorization =
Bearer ${response.data.accessToken};
return api(originalRequest);
} catch (err) {
// Refresh 실패, 로그아웃
localStorage.clear();
window.location.href = '/login';
return Promise.reject(err);
}
}
return Promise.reject(error);
}
);
export default api;
Python 구현
PyJWT
pip install PyJWT
import jwt
import datetime
SECRET = 'your-secret-key'
# 토큰 생성
payload = {
'sub': '12345',
'name': '홍길동',
'role': 'admin',
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),
'iat': datetime.datetime.utcnow()
}
token = jwt.encode(payload, SECRET, algorithm='HS256')
print(f'토큰: {token}')
# 토큰 검증
try:
decoded = jwt.decode(token, SECRET, algorithms=['HS256'])
print(f'유효한 토큰: {decoded}')
except jwt.ExpiredSignatureError:
print('토큰 만료')
except jwt.InvalidTokenError:
print('유효하지 않은 토큰')
Flask
from flask import Flask, request, jsonify
import jwt
import datetime
from functools import wraps
app = Flask(__name__)
SECRET = 'your-secret-key'
def token_required(f):
@wraps(f)
def decorated(args, kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({'error': '토큰이 없습니다'}), 401
try:
token = token.split(' ')[1] # Bearer 제거
data = jwt.decode(token, SECRET, algorithms=['HS256'])
request.user = data
except jwt.ExpiredSignatureError:
return jsonify({'error': '토큰 만료'}), 401
except jwt.InvalidTokenError:
return jsonify({'error': '유효하지 않은 토큰'}), 401
return f(args, kwargs)
return decorated
@app.route('/api/login', methods=['POST'])
def login():
data = request.get_json()
# 사용자 검증 (예시)
if data['username'] == 'hong' and data['password'] == 'password':
token = jwt.encode({
'sub': '12345',
'username': data['username'],
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}, SECRET, algorithm='HS256')
return jsonify({'token': token})
return jsonify({'error': '인증 실패'}), 401
@app.route('/api/protected', methods=['GET'])
@token_required
def protected():
return jsonify({
'message': '보호된 라우트',
'user': request.user
})
보안 모범 사례
1. 비밀키 관리
// ❌ 나쁜 예
const secret = 'mysecret';
// ✅ 좋은 예
const secret = process.env.JWT_SECRET;
// 강력한 비밀키 생성
const crypto = require('crypto');
const secret = crypto.randomBytes(64).toString('hex');
2. 짧은 만료 시간
// Access Token: 15분
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
// Refresh Token: 7일
const refreshToken = jwt.sign(payload, refreshSecret, { expiresIn: '7d' });
3. HTTPS 사용
항상 HTTPS를 통해 토큰 전송
4. 민감한 정보 제외
// ❌ 나쁜 예
const token = jwt.sign({
password: 'secret123',
creditCard: '1234-5678'
}, secret);
// ✅ 좋은 예
const token = jwt.sign({
sub: userId,
role: 'user'
}, secret);
5. 토큰 블랙리스트
const blacklist = new Set();
function logout(req, res) {
const token = req.headers.authorization.split(' ')[1];
blacklist.add(token);
res.json({ message: '로그아웃 성공' });
}
function authenticateToken(req, res, next) {
const token = req.headers.authorization.split(' ')[1];
if (blacklist.has(token)) {
return res.status(401).json({ error: '무효화된 토큰' });
}
// 검증 계속...
}
6. 알고리즘 지정
// ✅ 알고리즘 명시
jwt.verify(token, secret, { algorithms: ['HS256'] });
// ❌ 알고리즘 미지정 (취약)
jwt.verify(token, secret);
RS256 (비대칭 암호화)
키 생성
# 개인키
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
# 공개키
openssl rsa -pubout -in private_key.pem -out public_key.pem
사용
const fs = require('fs');
const jwt = require('jsonwebtoken');
const privateKey = fs.readFileSync('private_key.pem');
const publicKey = fs.readFileSync('public_key.pem');
// 토큰 생성 (개인키)
const token = jwt.sign({ sub: '12345' }, privateKey, {
algorithm: 'RS256',
expiresIn: '1h'
});
// 토큰 검증 (공개키)
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256']
});
일반적인 실수
1. 토큰을 LocalStorage에 저장
문제: XSS 공격에 취약
해결:
// ✅ HttpOnly 쿠키 사용
res.cookie('token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000
});
2. 토큰 만료 확인 안함
// ✅ 항상 검증
try {
const decoded = jwt.verify(token, secret);
} catch (error) {
// 만료 처리
}
3. 약한 비밀키
// ❌ 약한 키
const secret = 'secret';
// ✅ 강한 키
const secret = crypto.randomBytes(64).toString('hex');
결론
JWT는 강력하고 유연한 인증 메커니즘입니다. 올바르게 구현하면 안전하고 확장 가능한 인증 시스템을 구축할 수 있습니다.
핵심 요약:**
- ✅ 짧은 만료 시간 설정
- ✅ Refresh Token 사용
- ✅ HTTPS 필수
- ✅ 민감한 정보 제외
- ✅ 강력한 비밀키 사용
지금 바로 JSON Simplify에서 JWT를 디코딩해보세요!
관련 글
JSON 파일: 구조와 사용법 완벽 가이드
JSON 파일에 대한 종합 가이드 - .json 확장자, MIME 타입, 구조 및 JSON 파일을 효과적으로 생성, 열기, 사용하는 방법을 배워보세요.
JavaScript와 JSON: 완벽한 가이드 2026
JavaScript에서 JSON을 마스터하세요. JSON.parse(), JSON.stringify(), 오류 처리, fetch API 및 모범 사례에 대한 완벽한 가이드입니다.
JSON API와 REST 서비스: 현대 웹 개발의 핵심
JSON API와 REST 서비스의 모든 것을 배워보세요. RESTful 설계 원칙, 실용적인 예제, 모범 사례까지 완벽 정리.