[Redis #2] JWT 인증 시스템에서 Redis 활용하기

Refresh Token 보안 구조 설계와 구현


1. 들어가며

JWT 기반 인증 시스템에서 Refresh Token 관리는 보안성과 사용자 경험의 핵심 요소입니다.
특히 다중 인스턴스 환경이나 서버 측 토큰 제어가 필요한 경우, 토큰 저장소의 도입이 중요합니다.

Redis를 활용한 화이트리스트 방식 Refresh Token 관리 구조와 Spring Boot로의 실제 구현을 설명합니다.


2. 왜 서버가 토큰을 저장해야 할까?

JWT는 기본적으로 Stateless하며, 서버는 토큰 검증만 수행합니다.
하지만 다음과 같은 요구사항이 있다면 서버가 토큰을 저장해야 합니다:

  • 강제 로그아웃, 계정 정지, 토큰 재사용 방지
  • 1인 1토큰 정책 (중복 로그인 방지)
  • 로그아웃 후 토큰 무효화
  • Rotation 기반 보안 강화

→ Refresh Token 저장소가 필요하고 Redis가 적합


3. 왜 Redis인가? 다른 대안들과 비교

Redis의 장점

  • 메모리 기반 → 빠른 성능
  • TTL 지원 → 자동 만료 처리
  • 다중 인스턴스 간 공유 가능
  • 키 검색(SCAN), 실시간 모니터링

비교 표

항목 Redis DB 저장 (RDB) JVM Memory (Map 등)
접근 속도 매우 빠름 느림 매우 빠름
공유 가능성 O O X
TTL 지원 O 일부 가능 X
유연성 높음 높음 낮음

4. 화이트리스트 방식 vs 블랙리스트 방식

항목 화이트리스트 방식 블랙리스트 방식
기본 개념 유효한 토큰만 저장 무효화된 토큰만 저장
저장소 부하 적음 (최신 토큰만 저장) 많음 (모든 만료 토큰 저장)
재사용 공격 대응 Rotation + 덮어쓰기로 탐지 가능 블랙리스트 확인 필요
적합한 상황 Refresh 보안, Rotation 필수 환경 Access 무효화 필요 시

👉 본 시스템은 화이트리스트 + Rotation 방식 사용


5. Refresh Token Redis 관리 구조 설계

저장 구조

  • Key: refresh:user:{email}
  • Value: JWT Refresh Token
  • TTL: 3일 (72시간)

전체 흐름

  1. 로그인 → access + refresh 발급
  2. refresh는 Redis에 저장 (기존 덮어쓰기)
  3. access 만료 → /reissue 요청
  4. Redis에서 refresh 조회 → 일치 확인
  5. 미일치 시 재사용 공격 간주
  6. 로그아웃 시 Redis 토큰 삭제

6. 실제 구현 (Spring Boot)

✅ Refresh Token 저장

public void saveRefreshToken(String email, String token) {
    redisTemplate.opsForValue().set("refresh:user:" + email, token, 3, TimeUnit.DAYS);
}

✅ Refresh Token 유효성 검증

public void validateRefreshToken(String token) {
    if (jwtUtil.isExpired(token)) {
        throw new TokenValidationException("Token expired");
    }

    if (!"refresh".equals(jwtUtil.getCategory(token))) {
        throw new TokenValidationException("Invalid token type");
    }

    String email = jwtUtil.getEmail(token);
    String saved = redisTemplate.opsForValue().get("refresh:user:" + email);
    if (!token.equals(saved)) {
        throw new TokenValidationException("Token not in whitelist");
    }
}

✅ 로그아웃 처리

public void removeRefreshToken(String token) {
    String email = jwtUtil.getEmail(token);
    redisTemplate.delete("refresh:user:" + email);
}

✅ 응답에 토큰 담기

public static void addTokensToResponse(HttpServletResponse response, TokenPair tokens) {
    response.setHeader("Authorization", "Bearer " + tokens.getAccessToken());
    response.addCookie(CookieUtil.createRefreshTokenCookie(tokens.getRefreshToken()));
}

7. 리팩토링 방향 및 고려사항

1️⃣ 역할 분리

  • TokenService → 토큰 생성/검증/저장
  • JWTUtil → JWT 생성 및 파싱
  • CookieUtil, TokenResponseUtil → 쿠키 생성 및 응답 구성

2️⃣ 예외 처리 통일

  • TokenValidationException → 모든 인증 실패 처리
  • @ControllerAdvice → 전역 예외 처리

3️⃣ 설정 외부화

  • TTL, Redis Key Prefix 등은 application.yml로 관리

4️⃣ 테스트 전략

  • Redis 없는 환경에선 Mock 또는 embedded-redis 사용

8. 마무리

Redis는 Refresh Token을 안전하게 저장하고, 보안적으로 검증할 수 있는 이상적인 저장소입니다.
화이트리스트 + Rotation 기반 설계를 통해 다음을 달성할 수 있습니다:

  • 재사용 공격 방지
  • 서버 주도 보안 제어
  • Stateless 아키텍처 유지