DevToolBox무료
블로그

JWT 보안 모범 사례: 알고리즘 선택, 만료, 순환 및 공격 방지

14분by DevToolBox

JWT 보안 모범 사례: 알고리즘 선택, 만료, 순환 및 공격 방어

JSON Web Token은 어디에나 있습니다 — 하지만 대부분의 구현에는 심각한 보안 결함이 있습니다. 이 가이드는 알고리즘 선택, 토큰 만료 및 순환 전략, 리프레시 토큰 패턴, 주요 JWT 공격에 대한 방어를 다룹니다.

JWT란 무엇이고 어떻게 작동하나요?

JWT는 점으로 구분된 세 부분으로 구성된 Base64URL 인코딩 문자열입니다: 헤더(알고리즘 + 유형), 페이로드(클레임), 서명.

알고리즘 선택: HS256 vs RS256 vs ES256

가장 흔한 JWT 보안 실수는 분산 시스템에서 HS256을 사용하는 것입니다. RS256과 ES256은 비대칭 키를 사용합니다.

// Anatomy of a JWT — Header.Payload.Signature

// 1. Header (Base64URL decoded)
const header = {
  alg: "RS256",   // Algorithm: RS256 (asymmetric) preferred over HS256 (symmetric)
  typ: "JWT"
};

// 2. Payload (Base64URL decoded) — the claims
const payload = {
  iss: "https://auth.example.com",         // Issuer
  sub: "user_01HXYZ123",                  // Subject (user ID)
  aud: "https://api.example.com",          // Audience (target service)
  exp: 1735689600,                         // Expiry: Unix timestamp (15 min from now)
  iat: 1735688700,                         // Issued at
  jti: "8f14e0f9-21af-4c1e-8f26-3b4a5c6d7e8f", // Unique token ID (for revocation)
  roles: ["user"],                         // Custom claims
  email: "alice@example.com"
};

// 3. Signature (server-side only)
// RS256: sign(base64url(header) + '.' + base64url(payload), PRIVATE_KEY)
// Verification: verify(token, PUBLIC_KEY) — other services only need the public key

// --- Generating a JWT with jsonwebtoken (Node.js) ---
import jwt from 'jsonwebtoken';
import { readFileSync } from 'fs';
import { randomUUID } from 'crypto';

const privateKey = readFileSync('./private.pem');   // RSA private key
const publicKey  = readFileSync('./public.pem');    // RSA public key (shared with all services)

function issueAccessToken(userId: string, roles: string[]): string {
  return jwt.sign(
    {
      sub: userId,
      aud: 'https://api.example.com',
      roles,
      jti: randomUUID(),
    },
    privateKey,
    {
      algorithm: 'RS256',
      issuer: 'https://auth.example.com',
      expiresIn: '15m',  // Short-lived access token
    }
  );
}

function verifyAccessToken(token: string) {
  return jwt.verify(token, publicKey, {
    algorithms: ['RS256'],           // CRITICAL: whitelist algorithms — never allow 'none'
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
  });
}

토큰 만료 및 순환 전략

단기 액세스 토큰(15분)과 장기 리프레시 토큰(7~30일)의 조합이 업계 표준입니다.

// Refresh Token Pattern — Stateless Access + Stateful Refresh

import jwt from 'jsonwebtoken';
import { randomBytes } from 'crypto';

// ---- Token issuance ----
async function issueTokenPair(userId: string) {
  const accessToken = jwt.sign(
    { sub: userId, jti: randomUUID() },
    process.env.JWT_PRIVATE_KEY!,
    { algorithm: 'RS256', expiresIn: '15m' }
  );

  // Refresh token: random bytes stored in DB — NOT a JWT
  const refreshToken = randomBytes(64).toString('hex');
  const refreshExpiry = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days

  // Store hashed refresh token in DB (never store plaintext)
  await db.refreshTokens.create({
    tokenHash: sha256(refreshToken),
    userId,
    expiresAt: refreshExpiry,
    createdAt: new Date(),
  });

  return { accessToken, refreshToken, refreshExpiry };
}

// ---- Token refresh endpoint ----
app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.cookies; // httpOnly cookie
  if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });

  const stored = await db.refreshTokens.findOne({
    tokenHash: sha256(refreshToken),
    expiresAt: { >: new Date() }, // Not expired
    revokedAt: null,                  // Not revoked
  });

  if (!stored) return res.status(401).json({ error: 'Invalid or expired refresh token' });

  // Rotate: invalidate old, issue new pair (refresh token rotation)
  await db.refreshTokens.update(
    { id: stored.id },
    { revokedAt: new Date() }         // Revoke old refresh token
  );

  const newPair = await issueTokenPair(stored.userId);

  // Set new refresh token as httpOnly cookie
  res.cookie('refreshToken', newPair.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    expires: newPair.refreshExpiry,
    path: '/auth/refresh',            // Scope cookie to refresh endpoint only
  });

  return res.json({ accessToken: newPair.accessToken });
});

// ---- Logout: revoke refresh token ----
app.post('/auth/logout', async (req, res) => {
  const { refreshToken } = req.cookies;
  if (refreshToken) {
    await db.refreshTokens.update(
      { tokenHash: sha256(refreshToken) },
      { revokedAt: new Date() }
    );
  }
  res.clearCookie('refreshToken', { path: '/auth/refresh' });
  return res.json({ success: true });
});

일반적인 JWT 공격 및 방어

가장 위험한 JWT 취약점: 알고리즘 혼동, RS256→HS256 혼동, 약한 시크릿, 누락된 exp 검증.

// JWT Attack Defenses — Code Patterns

// ---- ATTACK 1: Algorithm confusion ("none" bypass) ----
// VULNERABLE: never use verify() without specifying allowed algorithms
const VULNERABLE = jwt.verify(token, secret); // attacker can set alg: "none"

// SAFE: always whitelist algorithms
const SAFE = jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // Never include 'none', 'HS256' if you're using RS256
});

// ---- ATTACK 2: RS256 > HS256 confusion ----
// Attacker changes alg from RS256 to HS256 and signs with the PUBLIC key as the secret
// Defense: always specify algorithm on verify — never infer from the token header
// VULNERABLE:
const header = JSON.parse(atob(token.split('.')[0]));
jwt.verify(token, publicKey, { algorithms: [header.alg] }); // attacker controls this!

// SAFE:
jwt.verify(token, publicKey, { algorithms: ['RS256'] }); // hardcoded

// ---- ATTACK 3: Weak HS256 secrets (brute-force) ----
// VULNERABLE: short or predictable secrets
const BAD_SECRET = 'secret';
const BAD_SECRET2 = 'mysupersecretkey';

// SAFE: 256-bit cryptographically random secret
import { randomBytes } from 'crypto';
const GOOD_SECRET = randomBytes(32).toString('hex'); // 256-bit
// Store in environment variable, rotate annually

// ---- ATTACK 4: Missing expiry validation ----
// Some older libraries skip exp check by default
// SAFE: always validate exp (jsonwebtoken does this by default)
try {
  const decoded = jwt.verify(token, publicKey, {
    algorithms: ['RS256'],
    clockTolerance: 30, // Allow 30 seconds clock skew max
    ignoreExpiration: false, // NEVER set this to true in production
  });
} catch (err) {
  if (err instanceof jwt.TokenExpiredError) {
    return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
  }
  return res.status(401).json({ error: 'Invalid token' });
}

// ---- Token blacklist (for early revocation via jti) ----
import { createClient } from 'redis';
const redis = createClient();

async function revokeToken(jti: string, exp: number) {
  const ttl = exp - Math.floor(Date.now() / 1000); // seconds until natural expiry
  if (ttl > 0) {
    await redis.set(`jwt:revoked:${jti}`, '1', { EX: ttl });
  }
}

async function isTokenRevoked(jti: string): Promise<boolean> {
  return (await redis.get(`jwt:revoked:${jti}`)) !== null;
}

// Middleware: check blacklist
app.use(async (req, res, next) => {
  const decoded = jwt.verify(req.headers.authorization?.split(' ')[1] ?? '', publicKey, {
    algorithms: ['RS256'],
  });
  if (await isTokenRevoked((decoded as jwt.JwtPayload).jti ?? '')) {
    return res.status(401).json({ error: 'Token revoked' });
  }
  next();
});

안전한 토큰 저장

액세스 토큰: 메모리에 저장. 리프레시 토큰: httpOnly, Secure, SameSite=Strict 쿠키에 저장.

자주 묻는 질문

JWT와 불투명 세션 토큰 중 어떤 것을 사용해야 하나요?

서버 사이드 렌더링 앱에는 불투명 세션 토큰을 사용하세요. 마이크로서비스나 API에는 JWT를 사용하세요.

JWT를 만료 전에 취소하려면 어떻게 해야 하나요?

해결책: 짧은 만료 시간(15분), Redis의 토큰 블랙리스트, 사용자 레코드의 토큰 버전.

모든 JWT에 포함되어야 할 클레임은?

필수: iss, sub, aud, exp, iat, jti. 수신 서비스에서 항상 iss와 aud를 검증하세요.

JWT 시크릿과 개인 키의 길이는?

HS256 시크릿: 최소 256비트. RS256: 최소 2048비트 RSA 키. ES256: 256비트 타원 곡선 키.

관련 도구

𝕏 Twitterin LinkedIn
도움이 되었나요?

최신 소식 받기

주간 개발 팁과 새 도구 알림을 받으세요.

스팸 없음. 언제든 구독 해지 가능.

Try These Related Tools

JWTJWT Decoder→BBase64 Decoder{ }JSON Formatter

Related Articles

JWT 인증: 완벽 구현 가이드

JWT 인증을 처음부터 구현. 토큰 구조, 액세스 토큰과 리프레시 토큰, Node.js 구현, 클라이언트 측 관리, 보안 모범 사례, Next.js 미들웨어.

HTTP 헤더 완전 가이드: 요청, 응답 및 보안 헤더

HTTP 헤더 완전 가이드: 요청, 응답 및 보안 헤더에 대한 설명.

REST API 설계 가이드: 2026년 모범 사례

견고한 REST API 설계 종합 가이드: 리소스 명명, HTTP 메서드, 페이지네이션, 에러 처리, 인증, 캐싱, OpenAPI.

온라인 JWT 디코더: JSON Web Token 디코딩, 검사 & 디버그 (2026 가이드)

무료 온라인 JWT 디코더로 JWT 헤더, 페이로드, 클레임을 즉시 검사하세요. JWT 구조, 표준 클레임, JavaScript/Python/Go/Java 디코딩, 서명 알고리즘, 보안 모범 사례 포함.