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를 사용하는 것이 아직 익숙하고 완벽하지는 않지만, 충분히 재미있고 매력있는 강력한 기능이라는 생각이 든다.
읽어주셔서 감사합니다 😊
'해피 코딩 > Today I Learned' 카테고리의 다른 글
[TIL 22] 팀 프로젝트 물류 도메인 시스템 구조 (0) | 2024.09.06 |
---|---|
[TIL 21] 팀 프로젝트 ERD 다시 그려보기 (0) | 2024.09.04 |
[TIL 19] Ai가 대세! Gemini API 사용하기 (2) | 2024.09.01 |
[TIL 18] 프로젝트에 Swagger 적용하기 (4) | 2024.09.01 |
[TIL 17] 프로젝트 중간 회고 (0) | 2024.08.29 |