프로젝트를 진행하면서 로그아웃을 구현하면서 정리한 내용입니다.
• Access Token은 Stateless JWT 방식
• API 요청 시 인증 수단으로 사용
• 서버에 저장되지 않으며 만료 시간까지 유효
• Refresh Token은 로그인 상태 관리용 토큰
• Redis에 저장
• 계정(email)당 1개의 Refresh Token만 유지
• 로그아웃 및 토큰 재발급 시 사용
key : email
value : refreshToken
JWT 구조에서 로그아웃은 RefreshToken을 삭제하는 동작입니다. 즉, 토큰 재발급을 차단하는 역할이며, 기존 AccessToken은 만료 시점까지 유효합니다. 따라서 로그아웃은 로그인 상태의 연장을 차단하는 행위라고 생각하면 편합니다.
아래의 조건을 만족해야 로그아웃이 성공합니다.
- Refresh Token이 요청에 포함되어 있어야 합니다.
- Refresh Token이 JWT로서 유효해야 합니다.
- Redis에 해당 email의 Refresh Token이 존재해야 합니다.
- Redis에 저장된 Refresh Token과 요청으로 전달된 Refresh Token이 동일해야 합니다.
위 조건이 없다면 이전 로그인 토큰으로 현재 로그인 세션을 종료시키는 문제가 발생합니다.
@ApiImplicitParam(name = "X-REFRESH-TOKEN", value = "VERIFY JWT (SMS 인증 성공 후 받은 토큰)", required = true, dataType = "String", paramType = "header")
@PostMapping("/logout")
ResponseEntity<ResultDto> Logout(HttpServletRequest request){
ResultDto resultDto = signService.logout(request);
return ResponseEntity.status(HttpStatus.OK).body(resultDto);
}
public class SignServiceImpl implements SignService {
... 생략 ...
@Override
public ResultDto logout(HttpServletRequest request) {
try {
ResultDto resultDto = new ResultDto();
String refreshToken = jwtProvider.resolveRefreshToken(request);
if (refreshToken == null) {
throw new IllegalArgumentException("RefreshToken이 없습니다.");
}
// 1. JWT 자체 유효성
if (!jwtProvider.validRefreshToken(refreshToken)) {
throw new IllegalArgumentException("유효하지 않은 RefreshToken입니다.");
}
// 2. 이메일 추출
String email = jwtProvider.getUsernameFromRefreshToken(refreshToken);
if (email == null || email.isBlank()) {
throw new IllegalArgumentException("email이 존재하지 않습니다.");
}
RefreshToken savedRefreshToken = refreshTokenRepository.findByUsername(email)
.orElseThrow(() -> new IllegalArgumentException("이미 로그아웃된 사용자입니다."));
// 4. 현재 RT인지 비교 (이게 없어서 지금 성공 뜨는 거임)
if (!savedRefreshToken.getRefreshToken().equals(refreshToken)) {
throw new IllegalArgumentException("이미 만료된 RefreshToken");
}
refreshTokenRepository.deleteById(email);
resultDto.setDetailMessage("로그아웃 완료.");
setSuccess(resultDto);
return resultDto;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
... 생략 ...
• 로그아웃은 Access Token이 아닌 Refresh Token 기준으로 처리됩니다.
• Redis에 저장된 Refresh Token을 삭제하여 로그인 상태를 종료합니다.
• 중복 로그인 환경에서도 현재 로그인된 세션만 로그아웃 가능하도록 설계되었습니다.
- 요청 헤더에서 Refresh Token 추출
- Refresh Token 자체(JWT) 유효성 검증
- Refresh Token에서 사용자 식별자(email) 추출
- Redis에 저장된 Refresh Token 조회
- Redis에 저장된 Refresh Token과 요청 토큰 일치 여부 검증
- 검증 성공 시 Redis에서 Refresh Token 삭제
- 로그아웃 성공 응답 반환
중복 로그인 차단은 다음 원칙으로 동작합니다.
• 새로운 로그인 발생 시 기존 Refresh Token은 Redis에서 덮어쓰기 됩니다.
• 이전 로그인 세션은 더 이상 Refresh Token을 사용할 수 없습니다.
• Access Token은 만료 전까지 유효하지만, 세션 연장은 불가능합니다.
즉, 중복 로그인 차단은 즉시 연결을 끊는 방식이 아니라
이전 세션을 자연스럽게 종료시키는 방식입니다.
⸻
첫 번째 로그인입니다
1-1. 사용자가 로그인합니다.
1-2. 서버는 다음 토큰을 발급합니다.
• Access Token_A
• Refresh Token_A
1-3. Refresh Token_A는 Redis에 저장됩니다.
같은 계정으로 두 번째 로그인입니다
2-1. 동일한 계정으로 다시 로그인합니다.
2-2. 서버는 새로운 토큰을 발급합니다.
• Access Token_B
• Refresh Token_B
2-3. Redis에 저장된 Refresh Token을 덮어씁니다.

1번 계정의 RefreshToken으로 로그아웃 시도

2번 계정의 RefreshToken으로 로그아웃 시도

로그아웃 한 2번 계정으로 로그아웃 시도

마지막으로 JWT 환경에서의 로그아웃은 세션을 즉시 끊는 개념이 아니라, Refresh Token을 기준으로 로그인 상태의 연장을 차단하는 행위라는 점이 핵심입니다.