[Spring Boot & Redis] - JWT 로그아웃 구현하기

KangWook·2024년 9월 2일
0

JWT (JSON Web Token)를 이용한 인증 시스템에서 로그아웃 기능을 구현하는 방법 중 하나는 Redis를 사용하는 것입니다. 이 글에서는 Spring Boot와 Redis를 사용하여 JWT 로그아웃을 구현하는 방법에 대해 다룹니다.

1. JWT 로그아웃의 필요성

JWT는 서버가 상태를 저장하지 않는 Stateless 인증 방식입니다. 클라이언트는 JWT를 발급받아 이후 요청 시마다 이를 서버에 전달합니다. 하지만, 로그아웃을 하려면 발급된 JWT를 무효화해야 합니다. 그렇지 않으면 JWT의 유효 기간이 끝날 때까지 해당 토큰으로 계속 인증을 할 수 있습니다.

2. Redis를 사용한 JWT 무효화 방법

Redis는 매우 빠른 인메모리 데이터 저장소로, JWT의 무효화 정보를 저장하는 데 적합합니다. 로그아웃 시, 해당 JWT를 Redis에 저장하고, 이후 요청 시 이 토큰이 Redis에 있는지 확인하여 유효성을 판단할 수 있습니다.

3. Spring Boot 프로젝트 설정

3.1. 의존성 추가

먼저 build.gradle 파일에 Redis와 Spring Security, JWT 관련 의존성을 추가합니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'io.jsonwebtoken:jjwt:0.9.1'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
}

application.yml 파일에 Redis 설정을 추가합니다.

spring:
  redis:
    host: localhost
    port: 6379
  1. JWT 로그아웃 구현

4.1. 로그아웃 서비스 구현

JWT 로그아웃 시 토큰을 Redis에 저장하는 LogoutService 클래스를 만듭니다.

  @Override
    public String logout(String accessToken) {
        String email = jwtUtils.getUserEmailFromToken(accessToken);
        // RefreshToken 삭제
        redisTemplate.delete("RT:" + email);
        return null;
    }
  1. String email = jwtUtils.getUserEmailFromToken(accessToken);

이 라인은 전달받은 AccessToken에서 사용자의 이메일 주소를 추출합니다.

jwtUtils.getUserEmailFromToken(accessToken): JwtUtils 클래스의 메서드를 호출하여, JWT 토큰에서 사용자 정보를 파싱하여 이메일 주소를 추출합니다.

  1. redisTemplate.delete("RT:" + email);

이 부분은 추출한 이메일 주소를 키로 사용하여, Redis에 저장된 해당 사용자의 RefreshToken을 삭제합니다.

Redis: Redis는 인메모리 데이터 저장소로, 토큰 정보를 빠르게 저장하고 조회하는 데 사용됩니다.
redisTemplate.delete(“RT:” + email): Redis에서 RT:email 키로 저장된 값을 삭제합니다.

여기서 RT:는 Redis에서 RefreshToken을 구분하기 위한 접두사로 사용됩니다.
RefreshToken 삭제의 의미: 로그아웃 시 사용자의 RefreshToken을 삭제함으로써 해당 사용자가 더 이상 유효한 AccessToken을 재발급받을 수 없도록 만듭니다. 즉, 로그아웃한 사용자는 다시 로그인을 하지 않는 한, 새로운 AccessToken을 받을 수 없습니다.

3.return null;

이 부분은 메서드의 반환 타입이 String이지만, 특별히 반환할 값이 없음을 의미합니다. 실제로 로그아웃 작업이 성공적으로 수행된 경우, 반환할 값이 없기 때문에 null을 반환합니다.

4.2. JWT 유효성 검사 필터

로그아웃된 토큰을 체크하는 JWT 필터를 추가합니다.

 @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String authHeader = request.getHeader(JwtUtils.AUTHORIZATION_HEADER);

        if (authHeader != null && authHeader.startsWith(JwtUtils.BEARER_PREFIX)) {
            String token = authHeader.substring(JwtUtils.BEARER_PREFIX.length());
            log.info("token : {}", token);
            try {
                if (jwtUtils.validateToken(token)) {
                    String email = jwtUtils.getUserEmailFromToken(token);
                    // RefreshToken 존재 여부 확인
                    String refreshToken = redisTemplate.opsForValue().get("RT:" + email);
                    if (refreshToken == null) {
                        throw new JwtException("로그아웃된 사용자입니다.");
                    }
                    // 요청 메서드가 PUT 또는 DELETE일 때만 자신의 이메일 확인
                    if (("PUT".equalsIgnoreCase(request.getMethod()) || "DELETE".equalsIgnoreCase(request.getMethod()))) {
                        String requestedEmail = request.getParameter("email"); // URL 파라미터에서 이메일 추출
                        if (requestedEmail == null || !requestedEmail.equalsIgnoreCase(email)) {
                            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                            response.getWriter().write("접근이 거부되었습니다. 자신의 이메일이 아닙니다.");
                            return;
                        }
                    }
                    // 인증된 사용자 및 역할을 요청 속성에 설정
                    request.setAttribute("AuthenticatedUser", email);
                } else {
                    throw new JwtException("유효하지 않거나 이미 만료된 토큰입니다.");
                }
            } catch (JwtException e) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write("JWT 인증 실패: " + e.getMessage());
                return;
            }
        } else if (!isExcludedPath(request.getRequestURI())) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.getWriter().write("토큰이 누락되었습니다.");
            return;
        }
        filterChain.doFilter(request, response);
    }
  1. 로그아웃 컨트롤러

로그아웃 요청을 처리하는 API를 작성합니다.

@PostMapping("/logout")
    public ResponseEntity<String> logout(@RequestHeader("Authorization") String token) {
        String expiredToken = userService.logout(token.replace(JwtUtils.BEARER_PREFIX, ""));
        return ResponseEntity.ok()
                .header(JwtUtils.AUTHORIZATION_HEADER, JwtUtils.BEARER_PREFIX + expiredToken)
                .body("로그아웃 되었습니다.");
    }

Redis를 활용하여 JWT 기반 인증 시스템에서 로그아웃 기능을 구현하는 방법을 알아보았습니다. 이 방법은 토큰의 만료 시간까지 로그아웃된 토큰을 추적할 수 있어, 사용자가 로그아웃한 후에도 안전하게 시스템을 보호할 수 있습니다. Redis의 빠른 성능 덕분에 성능 저하 없이 무효화된 토큰을 관리할 수 있습니다.

참고 자료
Spring Boot 공식 문서 https://spring.io/projects/spring-boot
Redis 공식 문서 https://redis.io/docs/latest/

profile
꾸준히 성장하는 개발자

0개의 댓글