Node.js · 2026-01-16

JWT 리프레시 토큰과 재발급 전략 설계

토큰 기반 인증에서 JWT 리프레시 토큰의 역할과 보안 고려사항, 토큰 회전·재발급 전략 및 Node.js 구현 예제를 포함한 설계

작성일 : 2026-01-16 ㆍ 작성자 : 관리자
post
목차

소개

JWT를 이용한 인증에서 액세스 토큰은 짧은 수명으로 자주 만료된다. 따라서 사용자 경험을 유지하면서 보안을 확보하려면 리프레시 토큰을 함께 설계해야 한다. 이 글은 JWT 리프레시 토큰의 개념과 위협 모델, 안전한 저장 방법, 그리고 실제로 적용 가능한 Node.js 토큰 재발급 전략을 설명한다.

JWT와 리프레시 토큰의 역할

액세스 토큰과 리프레시 토큰 구분

액세스 토큰은 API 접근 권한을 증명하며 수명이 짧다. 반면 리프레시 토큰은 액세스 토큰을 새로 발급받기 위한 자격증명으로 더 긴 수명을 갖는다. 중요한 점은 리프레시 토큰 자체가 탈취되면 장기적으로 계정이 위험해진다는 점이다.

왜 리프레시 토큰이 필요한가

  • 짧은 액세스 토큰으로 노출 시간 최소화
  • 사용자 재로그인 없이 토큰 갱신 가능
  • 세션 제어와 폐기 정책 구현 용이

보안 고려사항

저장 위치

클라이언트 저장소는 보안성과 편의성 사이의 절충이다. 웹에서는 HttpOnly, Secure 속성의 쿠키에 리프레시 토큰을 두는 것이 CSRF/ XSS 공격을 줄이는 데 유리하다. 모바일 환경에서는 안전한 키체인이나 Keystore 사용을 권장한다.

토큰 회전(Token Rotation)

토큰 회전은 리프레시 토큰을 사용해 새로운 리프레시 토큰을 발급하고 이전 토큰을 무효화하는 방식이다. 회전 전략은 도난된 토큰의 재사용을 탐지하는 데 효과적이며, 재발급 시마다 상태를 갱신해 장기 노출 위험을 낮춘다.

재발급 시 재사용 감지

만약 이미 사용된 리프레시 토큰으로 재발급 요청이 들어오면 이는 토큰 탈취 가능성을 의미한다. 이 경우 모든 세션을 무효화하거나 추가 인증을 요구하는 방식으로 대응한다.

토큰 폐기와 상태 관리

JWT는 본래 상태 비저장(stateless)이지만 리프레시 토큰을 안전하게 운용하려면 최소한의 상태 저장이 필요하다. 일반적인 방법은 Redis 같은 인메모리 DB에 토큰 블랙리스트나 핸들링 정보를 보관하는 것이다.

  • 발급 시: 리프레시 토큰의 식별자(jti)와 만료 시간 저장
  • 재발급 시: 이전 jti를 블랙리스트에 추가하고 새 jti 저장
  • 로그아웃 시: 현재 jti를 즉시 블랙리스트에 추가

Node.js 토큰 재발급 전략 예제

아래 예제는 간단한 flow를 보여준다. 핵심은 리프레시 토큰의 jti를 DB에 저장하고, 재발급 시 회전 및 재사용 탐지를 수행하는 것이다. 이 코드는 교육 목적이며 실제 배포 전 추가 검증과 예외 처리가 필요하다.

const express = require('express')
const jwt = require('jsonwebtoken')
const { v4: uuidv4 } = require('uuid')
// 가정: redisClient는 async get/set/del 지원

// 발급 함수
function issueTokens(userId) {
  const access = jwt.sign({ sub: userId }, 'ACCESS_SECRET', { expiresIn: '15m' })
  const jti = uuidv4()
  const refresh = jwt.sign({ sub: userId, jti }, 'REFRESH_SECRET', { expiresIn: '7d' })
  // redis에 jti 저장: key=jti, value=userId, ttl=7일
  redisClient.set(jti, userId, 'EX', 7 * 24 * 60 * 60)
  return { access, refresh }
}

// 리프레시 엔드포인트
app.post('/auth/refresh', async (req, res) => {
  const { refresh } = req.body
  try {
    const payload = jwt.verify(refresh, 'REFRESH_SECRET')
    const { sub: userId, jti } = payload
    const stored = await redisClient.get(jti)
    if (!stored) return res.status(401).json({ error: 'invalid_refresh' })

    // 토큰 회전: 기존 jti 삭제, 새 토큰 발급
    await redisClient.del(jti)
    const tokens = issueTokens(userId)
    return res.json(tokens)
  } catch (err) {
    return res.status(401).json({ error: 'invalid_token' })
  }
})

설명

  • 발급 시 jti를 저장해 유효성을 검증한다.
  • 재발급 시 기존 jti를 삭제해 재사용을 차단한다.
  • 재사용 의심이 있으면 추가 조치(세션 종료, 알림 등)를 실행한다.

운영 중 고려사항

로그아웃, 비밀번호 변경, 의심스러운 활동 발생 시 리프레시 토큰을 즉시 폐기할 방안을 마련한다. 또한 토큰 시크릿 관리, 키 롤링, 만료 정책을 명확히 두어 긴급 시 전체 시스템의 토큰을 회수할 수 있어야 한다.

결론

JWT 리프레시 토큰 구현은 단순히 긴 만료 시간을 주는 것이 아니다. 회전, 저장, 폐기, 재사용 탐지 등 여러 요소를 조합해 보안을 확보해야 한다. Node.js 토큰 재발급 전략은 Redis 같은 상태 저장소와 함께 토큰 회전을 적용하면 현실적인 보안 수준을 달성할 수 있다. 실제 구현 시에는 refresh token Node.js 예제와 같은 기본 패턴을 바탕으로 조직의 위협 모델에 맞춘 추가 방어를 설계하는 것이 중요하다.

JWT 리프레시 토큰 JWT 리프레시 토큰 구현 Node.js 토큰 재발급 전략 refresh token Node.js 예제 토큰 회전 토큰 폐기 Redis 세션 관리