오늘은 Spring Boot에서 JWT 인증 방식을 사용하는 환경에서 Redis를 활용한 로그아웃 처리 및 Refresh Token 관리에 대해 정리했다. 구현하며 느꼈던 포인트들과 코드 중심으로 정리해본다.
JWT는 서버에 세션을 저장하지 않는 Stateless 방식이라 로그아웃 시 토큰을 무효화시키는 것이 쉽지 않다.
이를 해결하기 위해 Redis를 블랙리스트 저장소로 활용한다.
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(genericJackson2JsonRedisSerializer());
return template;
}
private void authenticate(HttpServletRequest request) {
String token = this.getTokenFromHeader(request);
if (!StringUtils.hasText(token) || !jwtUtil.validateToken(token)) return;
// 블랙리스트 여부 확인
String isLogout = (String) redisTemplate.opsForValue().get(token);
if ("logout".equals(isLogout)) return;
String email = jwtUtil.getUserEmail(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
@Override
public void logout(String accessToken, Long expirationTime) {
// 블랙리스트 등록
redisTemplate.opsForValue().set(accessToken, "logout", Duration.ofMillis(expirationTime));
// Refresh Token 삭제
redisTemplate.delete("RT:" + getEmailFromToken(accessToken));
}
@Override
public void saveRefreshToken(String email, String refreshToken, Long ttl) {
redisTemplate.opsForValue().set("RT:" + email, refreshToken, Duration.ofMillis(ttl));
}
public String reissueAccessToken(String email, String providedRefreshToken) {
String savedRefreshToken = (String) redisTemplate.opsForValue().get("RT:" + email);
if (!providedRefreshToken.equals(savedRefreshToken)) {
throw new CustomException(TokenErrorCode.INVALID_REFRESH_TOKEN);
}
return jwtUtil.generateAccessToken(userRepository.findByEmail(email).get());
}
처음에는 Stateless 인증 구조에서 로그아웃 구현이 어렵게 느껴졌지만, Redis를 활용한 블랙리스트 패턴과 Refresh Token 저장 방식으로 충분히 실용적인 방법이 가능하다는 걸 깨달았다. Spring Security와 잘 연동되도록 설계하는 것이 핵심이었다.