전체 목록
보안Medium#39

JWT(JSON Web Token)란 무엇이며 세션 기반 인증과 어떻게 다른가요?

#보안#JWT#인증#세션
힌트

무상태성, 서버 부하, 토큰 무효화의 어려움을 생각해보세요.

정답 및 해설

JWT(JSON Web Token)란 무엇이며 세션 기반 인증과 어떻게 다른가요?

JWT(JSON Web Token)는 당사자 간에 정보를 JSON 객체로 안전하게 전달하기 위한 컴팩트하고 자가 포함적인 방식입니다. 서버에 상태를 저장하지 않고 토큰 자체에 인증 정보를 담아 전달하는 무상태(Stateless) 인증 방식입니다.

JWT의 구조

JWT는 .으로 구분된 세 부분으로 구성됩니다: Header.Payload.Signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIiwicm9sZSI6InVzZXIiLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

1. Header (헤더)

알고리즘과 토큰 타입을 정의합니다.

{
  "alg": "HS256",  // 서명 알고리즘 (HMAC SHA-256)
  "typ": "JWT"     // 토큰 타입
}

이를 Base64Url로 인코딩하면 첫 번째 부분이 됩니다.

2. Payload (페이로드)

실제 전달할 정보(Claim)를 담습니다. Base64Url로 인코딩되어 있어 누구나 디코딩 가능하므로 민감한 정보(비밀번호 등)는 절대 포함하면 안 됩니다.

{
  "userId": 123,
  "email": "user@example.com",
  "role": "user",
  "iat": 1700000000,   // 발급 시간 (issued at)
  "exp": 1700003600    // 만료 시간 (expiration)
}

등록된 Claim (표준)

Claim의미
iss발급자 (issuer)
sub주제 (subject), 보통 userId
exp만료 시간 (expiration)
iat발급 시간 (issued at)
nbf활성 시작 시간 (not before)

3. Signature (서명)

Header와 Payload를 비밀키로 서명하여 위변조를 방지합니다. 서버만 비밀키를 알고 있으므로, 서명이 유효하면 서버가 발급한 토큰임을 신뢰할 수 있습니다.

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  SECRET_KEY
)

JWT 인증 흐름

클라이언트                              서버
    |                                    |
    |--- POST /login (email, pw) ------->|
    |                                    | 1. 자격증명 검증
    |                                    | 2. JWT 생성 (서명 포함)
    |<-- 200 OK { token: "eyJ..." } -----|
    |                                    |
    | (토큰을 localStorage 또는 Cookie에 저장)
    |                                    |
    |--- GET /api/me                     |
    |    Authorization: Bearer eyJ... -->|
    |                                    | 3. 서명 검증
    |                                    | 4. Payload에서 userId 추출
    |<-- 200 OK { user: {...} } ---------|

Node.js에서 JWT 발급 및 검증

import jwt from 'jsonwebtoken';

const SECRET_KEY = process.env.JWT_SECRET;

// 토큰 발급 (로그인 시)
function generateToken(user) {
  return jwt.sign(
    { userId: user.id, email: user.email, role: user.role },
    SECRET_KEY,
    { expiresIn: '1h' }  // 1시간 후 만료
  );
}

// 토큰 검증 미들웨어
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // "Bearer <token>"

  if (!token) return res.status(401).json({ error: '토큰이 없습니다.' });

  jwt.verify(token, SECRET_KEY, (err, decoded) => {
    if (err) {
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({ error: '토큰이 만료되었습니다.' });
      }
      return res.status(403).json({ error: '유효하지 않은 토큰입니다.' });
    }
    req.user = decoded; // { userId, email, role }
    next();
  });
}

// 사용 예시
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findByEmail(email);
  if (!user || !await user.comparePassword(password)) {
    return res.status(401).json({ error: '잘못된 자격증명' });
  }
  const token = generateToken(user);
  res.json({ token });
});

app.get('/api/me', authenticateToken, (req, res) => {
  res.json({ user: req.user });
});

세션 기반 인증

세션 인증은 서버가 인증 상태를 메모리나 DB에 저장하고, 클라이언트에게는 세션 ID만 전달하는 방식입니다.

클라이언트                              서버
    |                                    |
    |--- POST /login (email, pw) ------->|
    |                                    | 1. 자격증명 검증
    |                                    | 2. 세션 생성 및 저장
    |                                    |    { sessionId: "abc123", userId: 1 }
    |<-- Set-Cookie: sessionId=abc123 ---|
    |                                    |
    |--- GET /api/me                     |
    |    Cookie: sessionId=abc123 ------>|
    |                                    | 3. 세션 저장소에서 abc123 조회
    |                                    | 4. userId 확인
    |<-- 200 OK { user: {...} } ---------|

Express.js 세션 인증 예시

import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';

const redisClient = createClient({ url: process.env.REDIS_URL });

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: true,
    maxAge: 1000 * 60 * 60, // 1시간
    sameSite: 'strict'
  }
}));

app.post('/login', async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  if (!user) return res.status(401).json({ error: '인증 실패' });

  // 세션에 사용자 정보 저장
  req.session.userId = user.id;
  req.session.role = user.role;
  res.json({ message: '로그인 성공' });
});

app.post('/logout', (req, res) => {
  // 세션 즉시 무효화
  req.session.destroy();
  res.clearCookie('connect.sid');
  res.json({ message: '로그아웃 성공' });
});

JWT vs 세션 비교

항목JWT세션
상태무상태 (Stateless)상태 유지 (Stateful)
서버 저장소불필요필요 (메모리, Redis 등)
확장성수평 확장 용이세션 공유 필요 (Redis 등)
즉시 무효화어려움 (블랙리스트 필요)즉시 가능 (세션 삭제)
토큰 크기상대적으로 큼세션 ID만 (작음)
보안탈취 시 만료까지 유효서버에서 즉시 제어 가능
클라이언트 저장localStorage 또는 CookieCookie (HttpOnly)

JWT의 단점과 해결 방법

즉시 무효화 문제

JWT는 서버가 발급한 후 만료 전까지 항상 유효합니다. 사용자가 로그아웃하거나 계정이 정지되어도 토큰이 만료될 때까지 계속 사용 가능합니다.

해결책: Refresh Token + 짧은 Access Token

// Access Token: 짧은 유효 기간 (15분)
const accessToken = jwt.sign({ userId }, SECRET, { expiresIn: '15m' });

// Refresh Token: 긴 유효 기간, DB에 저장
const refreshToken = jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: '7d' });
await db.saveRefreshToken(userId, refreshToken);

// Access Token 갱신 엔드포인트
app.post('/refresh', async (req, res) => {
  const { refreshToken } = req.cookies;
  const stored = await db.findRefreshToken(refreshToken);
  if (!stored) return res.status(401).json({ error: '유효하지 않은 토큰' });

  const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
  const newAccessToken = jwt.sign({ userId: decoded.userId }, SECRET, { expiresIn: '15m' });
  res.json({ accessToken: newAccessToken });
});

// 로그아웃 시 Refresh Token 삭제 → 갱신 불가 → 사실상 무효화
app.post('/logout', async (req, res) => {
  await db.deleteRefreshToken(req.cookies.refreshToken);
  res.clearCookie('refreshToken');
  res.json({ message: '로그아웃 완료' });
});

언제 무엇을 선택해야 할까

MSA / 마이크로서비스 / 분산 시스템 → JWT
  → 서버 간 세션 공유 불필요, 무상태로 수평 확장 용이

단일 서버 / 빠른 로그아웃이 중요 → 세션
  → 즉각적인 권한 취소, 서버 측 완전한 제어

모바일 앱 / 서드파티 API 인증 → JWT
  → 다양한 클라이언트에서 쉽게 사용 가능

금융 / 의료 등 보안 우선 → 세션 (또는 짧은 JWT + Refresh)
  → 즉각적인 세션 무효화 필요

현대적인 아키텍처에서는 Access Token(짧은 만료) + Refresh Token(HttpOnly Cookie) 조합이 보안과 편의성을 균형 있게 제공하는 방식으로 널리 사용됩니다.