JWT 완벽 가이드 2025 - 토큰 기반 인증의 모든 것

JWT(JSON Web Token)의 개념부터 실무 활용, 보안까지 2025년 최신 정보로 완벽 정리

Fun Utils2025년 10월 25일15분 읽기

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):

클레임이름설명
issIssuer토큰 발급자
subSubject토큰 주제 (사용자 ID)
audAudience토큰 대상자
expExpiration Time만료 시간 (UNIX timestamp)
nbfNot Before토큰 활성화 시간
iatIssued At토큰 발급 시간
jtiJWT 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 복사: 디버깅을 위한 원본 형식 복사

사용법:

  • JWT 토큰을 입력창에 붙여넣기
  • 자동으로 디코딩 및 검증 결과 표시
  • 에러/경고 메시지 확인
  • 보안 체크리스트

    ✅ 필수 사항

    • [ ] 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로 쉽게 디버깅


    관련 도구:

    더 알아보기:

    💙 우리의 콘텐츠가 도움이 되셨나요?

    무료 블로그와 도구 개발을 지원해주실 수 있습니다.


    관련 글