본문 바로가기
해피 코딩/Today I Learned

[TIL 20] Redis를 사용하여 로그아웃 블랙리스트 처리하기

by happy-coding 2024. 9. 3.

 Today I Learned

로그아웃 구현을 시작하기 전에는 단순히 Redis Cache에 있는 사용자의 정보를 삭제하고, 로그인 시 다시 캐싱해 주면 되겠지? 라고 생각했었지만 막상 구현을 시작하니 내 생각과는 많이 달랐다.
왜냐하면 로그아웃 시 정보를 삭제하는 것이 아닌 블랙리스트 처리를 해줘야 한다는 것을 처음 알게 되었기 때문이다.
🤔 그렇다면 왜 로그아웃 시 토큰을 블랙리스트로 처리하는 걸까?
💁 사용자가 로그아웃했음에도 불구하고 JWT는 여전히 유효하기 때문에 블랙리스트 저장소를 만들어서 관리해야 한다.
  • 목표
🔥 로그아웃 시 Cache Blacklist 처리하기

build.gradle
//Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

[ 로그인 ]

Service
  • 로그인 시 @CachePut을 사용하여 Redis에 캐싱해줍니다.
// 사용자 로그인 확인 및 토큰 생성
@CachePut(cacheNames = "userCache", key = "#requestDto.username")
public CacheDto login(LoginRequestDto requestDto) {
  // 사용자 로그인 확인
  String username = requestDto.getUsername();
  String password = requestDto.getPassword();

  // 사용자 조회
  User checkUser = userRepository.findByUsername(username).orElseThrow(() ->
          new IllegalArgumentException("존재하지 않는 사용자 입니다."));

  // 비밀번호 검증
  if (!passwordEncoder.matches(password, checkUser.getPassword())) {
    throw new IllegalArgumentException("비밀번호가 일치하지 않습니다");
  }

  // 확인된 사용자 토큰 생성
  String token = createToken(checkUser);

  // 로그인한 정보를 바탕으로 Redis에 캐싱
  return new CacheDto(
          checkUser.getUsername(),
          String.valueOf(checkUser.getRole()),
          token
  );
}

 

로그인
  • Postman을 사용하여 로그인 시도

  • 로그인 한 사용자 Redis에 캐싱해 주기 


[ 로그아웃 ]

Controller
@PostMapping("/logout")
public ResponseEntity<String> logout(
        HttpServletRequest request
) {
  // "Bearer " 이후의 토큰 값만 추출
  String token = extractTokenFromRequest(request);
  // 사용자 로그아웃 처리
  authService.logout(token);

  return ResponseEntity.ok("로그아웃 성공");
}
// "Bearer " 이후의 토큰 값만 추출
private String extractTokenFromRequest(HttpServletRequest request) {
  String bearerToken = request.getHeader("Authorization");
  if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
    return bearerToken.substring(7);
  }
  throw new IllegalArgumentException("유효한 토큰이 없습니다.");
}

 

Service
  • 로그아웃 시 "blacklist:토큰 값" 형식으로 Redis에 캐싱하여 블랙리스트 처리를 해줍니다.
  • 이후 블랙리스트된 토큰이 접근 시 Filter에서 Redis에 캐싱된 정보를 확인 후 일치하면 해당 토큰의 접근을 방어합니다.
private final RedisTemplate<String, String> redisTemplate;
private static final String TOKEN_BLACKLIST_PREFIX = "blacklist:";
// 로그아웃, 토큰 blacklist 등록
public void logout(String token) {

  // 토큰 검증 -> false 반환시 유효하지 않은 토큰
  if(!jwtRequestFilter.validateToken(token){
    throw new IllegalArgumentException("토큰이 유효하지 않습니다.");
  }
  AddBlacklist(token);
}

// 블랙리스트에 토큰 추가
public void AddBlacklist(String token){
  redisTemplate.opsForValue().set(TOKEN_BLACKLIST_PREFIX + token, "true");
  // 1시간 동안 블랙 리스트에서 해당 토큰을 관리
  redisTemplate.expire(TOKEN_BLACKLIST_PREFIX + token, 1, TimeUnit.HOURS);
}

[ 블랙리스트인지 검증하는 API ]

  • 토큰이 블랙리스트에 있을 경우 "403 NOT_FOUND" , 없을 경우 "200 OK" 반환
Filter
  • OncePerRequestFilter를 상속받는 JwtRequestFilter에 블랙리스트를 확인하는 메서드를 추가해 준다.
private final RedisTemplate<String, String> redisTemplate;
// 블랙리스트 확인 메서드
public boolean isTokenBlacklisted(String token) {
    return redisTemplate.hasKey("blacklist:" + token);
}

 

Service
// 토큰 블랙리스트 검증
public boolean isTokenBlacklisted(String token) {
  return jwtRequestFilter.isTokenBlacklisted(token);
}

 

Contorller
// 토큰이 블랙리스트인지 검증하는 API
@GetMapping("/blacklist")
public ResponseEntity<?> blacklist(
        HttpServletRequest request
) {
  // "Bearer " 이후의 토큰 값만 추출
  String token = extractTokenFromRequest(request);

  boolean isBlacklisted = authService.isTokenBlacklisted(token);
  // 토큰이 블랙리스트에 있을경우 "403 NOT_FOUND" , 없을 경우 "200 OK" 반환
  if (isBlacklisted) {
    return ResponseEntity.status(HttpStatus.NOT_FOUND).body("토큰이 블랙리스트에 있습니다.");
  } else {
    return ResponseEntity.status(HttpStatus.OK).body("토큰이 블랙리스트에 없습니다.");
  }
}

[ 결과 확인 ]

로그인

  • 로그인 한 사용자 Redis에 캐싱해 주기 

 

블랙리스트 검증
  • 유효한 토큰 200OK

 

로그 아웃
  • 로그인한 사용자의 토큰을 Headers에 넣어서 로그아웃

  • 로그아웃 시 해당 토큰을 Redis에 블랙리스트 등록

블랙리스트 검증
  • 블랙리스트 처리된 토큰 404 Not Found


로그아웃은 블랙리스트 사용하여 처리한다는 것을 처음 알게 되었으며, Redis를 사용하는 것이 아직 익숙하고 완벽하지는 않지만, 충분히 재미있고 매력있는 강력한 기능이라는 생각이 든다.

 

읽어주셔서 감사합니다 😊