Spring Boot에서 JWT 인증 운영하기
Logout Timestamp 기반 Access Token 무효화 + Redis 활용 전략
JWT 기반 인증을 도입하면 인증 로직은 단순해지지만,
로그아웃 처리와 토큰 무효화는 곧바로 운영 이슈로 이어진다.
특히 Access Token은 stateless 구조이기 때문에
“이미 발급된 토큰을 어떻게 즉시 무효화할 것인가?”는
JWT를 실제 서비스에 적용할 때 반드시 고민해야 할 문제다.
이 글에서는 Spring Boot 기반 API에서
- Access / Refresh Token 분리 설계
- Redis 기반 Refresh Token 관리
- Logout Timestamp 기반 Access Token 무효화 전략
- SecurityFilterChain에 JWT 필터를 적용하는 방식

1️⃣ 전체 인증 구조 개요
이번 구현의 핵심 목표는 다음과 같다.
- JWT 기반 Stateless 인증 유지
- Refresh Token은 서버(Redis)에서 통제
- Access Token은 저장하지 않되, 로그아웃 이후 즉시 무효화
이를 위해 아래 구조를 사용했다.
- JwtTokenProvider : JWT 생성 및 검증
- JwtAuthenticationFilter : 요청 단 JWT 인증 필터
- AuthService : 로그인 / 로그아웃 / 토큰 재발급
- SecurityConfig : Spring Security 설정
2️⃣ Access / Refresh Token 분리 설계
✅ Access Token
Access Token은 요청 인증용 토큰으로 사용한다.
- subject : userId
- custom claim : role
- 서명 방식 : HS256
- 만료 시간 : 30분
return Jwts.builder() .subject(userId) .claim("role", role) .issuedAt(now) .expiration(expiry) .signWith(secretKey, Jwts.SIG.HS256) .compact();
Access Token에는 최소한의 정보만 담고,
짧은 만료 시간으로 탈취 리스크를 줄인다.
✅ Refresh Token
Refresh Token은 재발급 전용 토큰이다.
- subject : userId
- role 정보 없음
- 만료 시간 : 7일
- Redis에 저장하여 서버가 직접 관리
Refresh Token은 반드시 서버 상태와 함께 관리해야 하므로
Redis를 사용해 단일 토큰만 유지하도록 설계했다.
3️⃣ 토큰 만료 설정
- Access Token → 짧게
- Refresh Token → 길게 + Redis 검증
운영 측면에서 가장 안정적인 조합이다.
4️⃣ 로그인 시 동작 흐름
로그인 성공 시 처리 흐름은 다음과 같다.
- 사용자 조회 및 비밀번호 검증
- 사용자 상태(ACTIVE) 확인
- Access / Refresh Token 발급
- Refresh Token을 Redis에 저장
- Redis에는 항상 가장 최신 Refresh Token 하나만 유지된다.
- 재로그인 시 이전 Refresh Token은 자동으로 무효화된다.
5️⃣ Refresh Token 재발급 흐름
Refresh API 요청 시 처리 순서:
- Refresh Token 서명 / 만료 검증
- 토큰에서 userId 추출
- 사용자 상태 확인
- Redis에 저장된 Refresh Token과 비교
- 새 Access / Refresh Token 발급
- Redis 갱신
이 구조를 통해:
- 탈취된 Refresh Token 차단
- 중복 로그인 관리
- 서버 주도 인증 흐름 유지
가 가능해진다.
6️⃣ Access Token 무효화 전략
Logout Timestamp 기반 방식
JWT의 한계는 Access Token을 즉시 폐기할 수 없다는 점이다.
이를 해결하기 위해
본 구현에서는 토큰을 블랙리스트로 저장하지 않고,
로그아웃 시점(timestamp)만 Redis에 저장하는 방식을 사용했다.
로그아웃 시 처리
Key: LO:{userId} Value: logout timestamp TTL: access token TTL
long now = System.currentTimeMillis(); redisTemplate.opsForValue().set( "LO:" + userId, String.valueOf(now), accessTtl, TimeUnit.MILLISECONDS );
이 값은 이후 모든 요청에서
Access Token 유효성 판단 기준으로 사용된다.
7️⃣ JWT 인증 필터 동작 흐름
JwtAuthenticationFilter는 모든 요청마다 다음 순서로 동작한다.
- Authorization 헤더에서 토큰 추출
- 서명 및 만료 검증
- Redis에서 로그아웃 시각 조회
- 토큰의 issuedAt(발급 시간)과 로그아웃 시각 비교
- 사용자 로딩
- SecurityContext에 인증 등록
🔍 핵심 무효화 로직
long issuedAtMillis = jwtTokenProvider.getIssuedAtMillis(token);
String lastLogoutAtStr = redisTemplate.opsForValue().get("LO:" + userId);
if (lastLogoutAtStr != null) {
long lastLogoutAt = Long.parseLong(lastLogoutAtStr);
if (issuedAtMillis < lastLogoutAt) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
}
👉 로그아웃 이전에 발급된 Access Token은 모두 무효 처리
👉 별도의 토큰 블랙리스트 저장 없이 동일한 효과를 낸다.
8️⃣ SecurityConfig 설정 요약
- CSRF / Form Login / HTTP Basic 비활성화
- Session 정책: STATELESS
- JWT 필터를 UsernamePasswordAuthenticationFilter 앞에 삽입
- 로그인 / 회원가입 / Swagger는 permitAll
- 그 외 API는 인증 필수
http.addFilterBefore(
new JwtAuthenticationFilter(jwtTokenProvider, redisTemplate),
UsernamePasswordAuthenticationFilter.class
);
9️⃣ API 응답 구조
로그인 및 재발급 API는 다음 형태로 응답한다.
{
"accessToken": "...",
"refreshToken": "...",
"tokenType": "Bearer",
"accessTokenExpiresIn": 1800000
}
프론트엔드에서 만료 시점을 예측하기 쉽도록 설계했다.
운영 관점에서의 장점
이 구조를 통해 얻은 장점은 다음과 같다.
- ✅ Access / Refresh Token 분리로 보안 강화
- ✅ Redis 기반 Refresh Token 비교로 탈취 방어
- ✅ Logout Timestamp 기반 Access Token 즉시 무효화
- ✅ Stateless 구조 유지 → 확장성 우수
- ✅ 토큰 저장 없는 효율적인 무효화 방식
Logout Timestamp 사용한 이유
전통적인 JWT 블랙리스트 방식은
로그아웃된 Access Token을 Redis에 직접 저장하여 관리한다.
그러나 본 프로젝트에서는 토큰을 저장하는 대신
로그아웃 시점(timestamp)만 저장하고 issuedAt(발급시간)과 비교하는 방식을 적용하여
Redis 메모리 사용량과 조회 비용을 크게 줄였다.
'자바 공부 > 스프링공부' 카테고리의 다른 글
| Mockito 기반 단위 테스트에서 검증 방법 정리 (0) | 2025.12.31 |
|---|---|
| 웹RTC(WebRTC)-1 (0) | 2025.12.04 |
| [스프링부트] SSE 사용법 코드 예제 (2) | 2025.06.14 |
| Spring ExceptionHandler을 사용한 예외처리 (0) | 2025.03.16 |
| 스프링 Junit5 와 Mockito를 이용한 단위 테스트 (0) | 2025.02.18 |