JWT + Redis 기반 로그아웃 구현

Yu Seong Kim·2026년 1월 17일

SpringBoot

목록 보기
33/33

프로젝트를 진행하면서 로그아웃을 구현하면서 정리한 내용입니다.

  • 기존에 jwrProvider 및 redis, refreshToken관련 설정 및 구현은 되어있는 상태입니다.
  • 로그아웃 동작 중심으로 정리하였습니다.

인증 구조 개요

• Access Token은 Stateless JWT 방식
• API 요청 시 인증 수단으로 사용
• 서버에 저장되지 않으며 만료 시간까지 유효
• Refresh Token은 로그인 상태 관리용 토큰
• Redis에 저장
• 계정(email)당 1개의 Refresh Token만 유지
• 로그아웃 및 토큰 재발급 시 사용

Redis 저장 구조

key   : email
value : refreshToken

JWT 구조에서 로그아웃

JWT 구조에서 로그아웃은 RefreshToken을 삭제하는 동작입니다. 즉, 토큰 재발급을 차단하는 역할이며, 기존 AccessToken은 만료 시점까지 유효합니다. 따라서 로그아웃은 로그인 상태의 연장을 차단하는 행위라고 생각하면 편합니다.

로그아웃 성공조건

아래의 조건을 만족해야 로그아웃이 성공합니다.

  1. Refresh Token이 요청에 포함되어 있어야 합니다.
  2. Refresh Token이 JWT로서 유효해야 합니다.
  3. Redis에 해당 email의 Refresh Token이 존재해야 합니다.
  4. Redis에 저장된 Refresh Token과 요청으로 전달된 Refresh Token이 동일해야 합니다.

위 조건이 없다면 이전 로그인 토큰으로 현재 로그인 세션을 종료시키는 문제가 발생합니다.


로그아웃 구현 코드

  • controller
    @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);
    }
  • signServiceImpl
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을 삭제하여 로그인 상태를 종료합니다.
• 중복 로그인 환경에서도 현재 로그인된 세션만 로그아웃 가능하도록 설계되었습니다.

로그아웃 처리 전체 흐름
  1. 요청 헤더에서 Refresh Token 추출
  2. Refresh Token 자체(JWT) 유효성 검증
  3. Refresh Token에서 사용자 식별자(email) 추출
  4. Redis에 저장된 Refresh Token 조회
  5. Redis에 저장된 Refresh Token과 요청 토큰 일치 여부 검증
  6. 검증 성공 시 Redis에서 Refresh Token 삭제
  7. 로그아웃 성공 응답 반환

중복 로그인 차단

중복 로그인 차단은 다음 원칙으로 동작합니다.
• 새로운 로그인 발생 시 기존 Refresh Token은 Redis에서 덮어쓰기 됩니다.
• 이전 로그인 세션은 더 이상 Refresh Token을 사용할 수 없습니다.
• Access Token은 만료 전까지 유효하지만, 세션 연장은 불가능합니다.
즉, 중복 로그인 차단은 즉시 연결을 끊는 방식이 아니라
이전 세션을 자연스럽게 종료시키는 방식입니다.

중복 로그인 전체 흐름

  1. 첫 번째 로그인입니다

    1-1. 사용자가 로그인합니다.
    1-2. 서버는 다음 토큰을 발급합니다.
    • Access Token_A
    • Refresh Token_A
    1-3. Refresh Token_A는 Redis에 저장됩니다.

  2. 같은 계정으로 두 번째 로그인입니다

    2-1. 동일한 계정으로 다시 로그인합니다.
    2-2. 서버는 새로운 토큰을 발급합니다.
    • Access Token_B
    • Refresh Token_B
    2-3. Redis에 저장된 Refresh Token을 덮어씁니다.

스웨거 테스트

  1. 동시에 같은 계정으로 로그인
  • 1번 -> 먼저 로그인
  • 2번 -> 나중에 로그인(제일 최근)
  1. 1번 계정의 RefreshToken으로 로그아웃 시도

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

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

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

profile
1년차 개발자의 Development Record Page

0개의 댓글