본문 바로가기
PlayGround/마실가실 리팩토링

[1년 후 마실가실] JWT와 로그아웃(3) 로그아웃

by HJ0216 2024. 9. 16.

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

스마트 루피를 데려온 이유는 스마트하게 로그아웃을 완성했기 때문입니다.

 

JWT와 로그아웃(1) 게시물을 8월 23일에 작성하였는데, 약 3주의 시간이 흘렀습니다,,

 

사실 구현은 이전에 했었는데, 멋진 코드를 붙여넣다보니 내 것인듯 내 것 아닌 내 것 같은 코드가 되어버려서 리리팩토링도 하고, SonarQube 등 설치도 하고.. 스프링 공부도 하고.. 그랬습니다..

 

 

서론은 여기서 마치고, 로그아웃 코드를 정리해보도록 하겠습니다.

 

https://hj0216.tistory.com/939

 

[1년 후 마실가실] JWT와 로그아웃(1) Redis 설정

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.   마실가실의 숨겨진 비밀..한 번 로그인 한 고객님은.. 서버를 내리기 전까지 로그아웃할 수 없습니다. 이제는

hj0216.tistory.com

https://hj0216.tistory.com/945

 

[1년 후 마실가실] JWT와 로그아웃(2) RefreshToken

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.  이것저것 좋아보이는 코드를 붙여넣다보니, 이도저도 아닌 누구세요 코드가 되어 정리를 좀 했습니다,,이름하여

hj0216.tistory.com

 

UserController.java
@RestController
@RequestMapping("api/v2/users")
@RequiredArgsConstructor
public class UserController {
    @PostMapping("/logout")
    @ResponseStatus(HttpStatus.OK)
    public void logout(@RequestBody TokenInfo logoutRequestDto){
        userService.logout(logoutRequestDto);
    }
}

 

UserService.java
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class UserService {
    public void logout(TokenInfo logoutRequestDto) {
        if (!jwtTokenProvider.isValidAccessToken(logoutRequestDto.getAccessToken())) {
            throw new BusinessException(INVALID_ACCESS_TOKEN);
        }

        if (redisUtil.get("RT:" + logoutRequestDto.getRefreshToken()) != null) {
            redisUtil.delete("RT:" + logoutRequestDto.getRefreshToken());
        }

        Long expiration = jwtTokenProvider.getExpiration(logoutRequestDto.getAccessToken());
        redisUtil.setBlackList("AT:" + logoutRequestDto.getAccessToken(), "logout", expiration);
    }
}

1. 로그아웃 요청 전 Access Token의 유효성 검증

2. Refresh Token을 통한 Access Token 재발급을 막기 위해 Redis에 저장된 Refresh Token이 있으면 삭제

3. Redis에 AccessToken을 BlackList 방식으로 저장하여 해당 토큰을 기반으로 한 Login 방지

* BlackList 방식

특정 토큰을 무효화하기 위해 그 토큰을 블랙리스트에 추가하는 것

 

JwtTokenProvider.java
@Component // Spring이 이 클래스를 자동으로 스캔하고 빈으로 등록
@RequiredArgsConstructor
public class JwtTokenProvider {
    // 토큰 정보를 검증하는 메서드
    public boolean isValidAccessToken(String accessToken) {
        if(redisUtil.hasKeyBlackList("AT:" + accessToken)){
            return false;
        }

        try {
            parseClaims(accessToken);

            return true;
        } catch (MalformedJwtException e) {
            throw new BusinessException(MALFORMED_JWT);
        } catch (ExpiredJwtException e) {
            throw new BusinessException(EXPIRED_JWT);
        } catch (UnsupportedJwtException e) {
            throw new BusinessException(UNSUPPORTED_JWT);
        } catch (IllegalStateException e) {
            throw new BusinessException(ILLEGAL_STATE_JWT);
        }
    }

    private Claims parseClaims(String token) {
        Key secretKey = Keys.hmacShaKeyFor(jwtSecretKey.getBytes(StandardCharsets.UTF_8));

        return Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    public Long getExpiration(String token) {
        Key secretKey = Keys.hmacShaKeyFor(jwtSecretKey.getBytes(StandardCharsets.UTF_8));

        // Token 남은 유효시간
        Date expiration = Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getExpiration();

        // 현재 시간
        Long now = new Date().getTime();

        return (expiration.getTime() - now); // TimeUnit.MILLISECONDS
    }
}

Access Token의 유효성 검사를 통해

  * 로그아웃한 사용자인지 확인

  * token을 통해 claims(JWT 를 이용해 전송되는 암호화된 정보) 추출 시, Exception 발생 여부 확인

 

JwtAuthenticationFilter.java
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    // JWT 토큰의 인증 정보를 현재 쓰레드의 SecurityContext에 저장하는 역할 수행
    @Override
    public void doFilterInternal(HttpServletRequest request
            , HttpServletResponse response
            , FilterChain filterChain
    ) throws IOException, ServletException {
        String accessToken = SecurityUtils.resolveToken(request);

        if (accessToken != null) {
            if(jwtTokenProvider.isValidAccessToken(accessToken)){
                Authentication authentication = jwtTokenProvider.getAuthentication(accessToken);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            } else {
                throw new BusinessException(LOGOUT_MEMBER);
            }
        }

        filterChain.doFilter(request, response);
    }
}

Filter에서 AccessToken의 유효성 검사를 수행하도록 하여 Logout한 멤버인지 확인

 

⭐ 좀 더 생각해봐야 할 부분 ⭐

* 필터에 유효성 검사를 추가하면 무조건 검사가 진행되는데, JwtTokenProvider에 추가적으로 수행한다면 중복 로직이 아닐까?

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

https://slimeland.tistory.com/41

 

[개발]JWT 갱신 토큰 관리 방법(5)-블랙리스트 사용

JWT (JSON Web Tokens) 인증 시스템에서 토큰을 무효화하는 방법 중 하나는 블랙리스트(blacklist)를 사용하는 것입니다. 이 방법은 특정 토큰을 무효화하기 위해 그 토큰을 블랙리스트에 추가하는 것을

slimeland.tistory.com

https://jake-seo-dev.tistory.com/77

 

JWT(Json Web Token) 이란 무엇이며 왜 사용하는가?

JWT (JSON Web Token) 란 무엇인가? open standard 로 RFC 7519 에 표준에 관한 내용이 기재되어 있다. 통신에 JSON 을 이용하여 JSON 객체를 통해 두 당사자 간 정보를 보안이 적용된 안전한 방식으로 전달한다.

jake-seo-dev.tistory.com