지난 포스트 중에서 Redis를 사용한 로그아웃을 구현한 적이 있다.
이번 프로젝트에서 Redis를 사용해 로그아웃을 구현하기 위해 지난 포스트를 보고 하려 했지만, 무언가 엉성한 코드가 가득하다는 것을 깨닫고 다시 작성해보려 한다.
Redis 관련 설정은 건너뛰고, 어떻게 Redis를 사용해 로그아웃을 구현할 수 있는지에 대해 포스팅하겠다.
RedisConfig 작성하기
Redis 관련 설정을 위해 RedisConfig 클래스를 생성해 보자.
@Configuration
public class RedisConfig {
//RedisTemplate 커스터마이징
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(host);
config.setPort(port);
return new LettuceConnectionFactory(config);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
// Key 직렬화 (String 타입으로 저장)
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// Value 직렬화 (JSON 형태로 저장하여 객체 저장 가능)
// GenericJackson2JsonRedisSerializer는 모든 객체를 JSON으로 직렬화/역직렬화할 수 있습니다.
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet(); // 설정 완료 후 초기화
return redisTemplate;
}
}
기본 RedisTemplate은 기본적으로 바이너리 형식으로 저장된다. 이로 인해, 연동의 어려움과 불필요한 데이터가 커짐 등의 이유로 직렬화를 통해 JSON 기반으로 바꾸기 위해 커스터마이징 한다.
- key는 문자열로 저장하고, value는 JSON으로 직렬화
- 저장할 때는 직렬화, 읽을 때는 역직렬화
※ 직렬화 : 객체(Object)를 Redis에 저장하거나 전송할 수 있도록 바이트(byte) 형태로 바꾸는 과정
로그아웃 구현
본격적으로 로그아웃 메서드를 작성해 보자.
String accessToken = extractAccessToken(httpServletRequest);
우선 AT을 요청 헤더에서 추출한다.
Long expiration = tokenProvider.getExpiration(accessToken);
if (expiration > 0) {
String blacklistKey = "blacklist:" + accessToken;
redisTemplate.opsForValue().set(
blacklistKey, // key
"logout", // value
expiration, // TTL
TimeUnit.MILLISECONDS // Unit
);
}
AT로부터 만료 시간을 추출한다.
왜 만료 시간이 long 타입일까?
JWT 표준 시간은 UNIX Time Stamp(1970년 1월 1일 이후 경과된 초 또는 밀리초)라는 큰 정수값으로 정의하기 때문에, 이를 받기 위해 long 타입을 사용하는 것이다.
아직 토큰이 만료되지 않았을 경우, AT을 블랙리스트 처리한다.
opsForValue()는 주로 Redis 자료구조 중 String에서 key-value 저장, TTL값 설정, 증가/감소, 값 조회에 사용된다.
여기서 이 함수는 "지정된 key-value를 저장하되, 만료 시간을 설정"한다.
String refreshToken = request.getRefreshToken();
if (refreshToken != null && !refreshToken.isEmpty()) {
// Refresh Token 검증
if (!tokenProvider.validateToken(refreshToken)) {
throw new InvalidTokenException("유효하지 않은 Refresh Token입니다.");
}
// Redis에서 Refresh Token 삭제
String refreshTokenKey = "refreshToken:" + username;
redisTemplate.delete(refreshTokenKey);
}
RT를 request body에서 가져온다. RT가 유효하다면, Redis에서 Refresh Token을 삭제한다.
전체 코드
@Transactional
public void logout(HttpServletRequest httpServletRequest, LogoutRequest request) {
String accessToken = extractAccessToken(httpServletRequest);
if (accessToken == null) {
throw new NotLoggedInException("로그인이 필요한 요청입니다.");
}
if (!tokenProvider.validateToken(accessToken)) {
throw new InvalidTokenException("유효하지 않은 Access Token입니다.");
}
String username = tokenProvider.extractUsername(accessToken);
Long expiration = tokenProvider.getExpiration(accessToken);
if (expiration > 0) {
String blacklistKey = "blacklist:" + accessToken;
redisTemplate.opsForValue().set(
blacklistKey,
"logout",
expiration,
TimeUnit.MILLISECONDS
);
}
String refreshToken = request.getRefreshToken();
if (refreshToken != null && !refreshToken.isEmpty()) {
// Refresh Token 검증
if (!tokenProvider.validateToken(refreshToken)) {
throw new InvalidTokenException("유효하지 않은 Refresh Token입니다.");
}
// Redis에서 Refresh Token 삭제
String refreshTokenKey = "refreshToken:" + username;
redisTemplate.delete(refreshTokenKey);
}
}
JwtAuthenticationFilter 설정
이제 사용자가 요청을 보낼 때 Spring Security는 사용자가 입력한 AT가 유효한지 확인할 때 블랙리스트 처리된 토큰인지도 확인해야 할 것이다. 이를 처리하기 위한 설정을 JwtAuthenticationFilter에서 작성한다.
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
if (tokenProvider.validateToken(token)) {
if (tokenProvider.isBlacklisted(token)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
"로그아웃된 토큰입니다.");
return;
}
...
}
}
토큰이 유효하다면 로그아웃된 토큰은 아닌지 확인하는 로직을 작성했다.
public boolean isBlacklisted(String accessToken) {
String blacklistKey = "blacklist:" + accessToken;
try {
Boolean hasKey = redisTemplate.hasKey(blacklistKey);
return Boolean.TRUE.equals(hasKey);
} catch (Exception e) {
System.out.println("블랙리스트 예외 발생");
return false;
}
}
이 메서드에서는 사용자의 AT를 Redis에서 가지고 있는지(블랙리스트 처리됐는지) 확인한다.

감사합니다 ( ̄﹃ ̄)
'Development > Back-end' 카테고리의 다른 글
| [Spring Boot] 어댑터 패턴 구현하기(with. JPA, Redis) (0) | 2025.12.26 |
|---|---|
| [Spring Boot] @Data 알고 쓰기 (0) | 2025.11.26 |
| [Spring Boot] JWT 서명 알고리즘 (2) - HS256 (0) | 2025.08.18 |
| [JWT] JWT 서명 알고리즘 (1) - HMAC, RSA (0) | 2025.08.18 |
| [Spring Boot] @Scheduled 사용하기 (0) | 2025.08.10 |
