Spring Security - JWT 기반 로그아웃

이유석·2024년 11월 22일
1

Spring-Security

목록 보기
10/10
post-thumbnail

로그아웃 시, AccessToken → BlackList 등록

  • 로그아웃 시, 해당 사용자의 AccessToken 은 더 이상 사용하면 안된다.
  • BlackList 로 관리해주며, 해당 AccessToken 을 사용하여 요청할 시, Access 를 반려시킨다.
  • BlackList 는 In-Memory Database 인 Redis 를 이용하여 구현하였다.

로그아웃 처리 순서

  1. 클라이언트에서 /user/logout(서버 로그아웃 API)으로 Http 헤더에 에 AccessToken 을 넣어서 요청한다.
  2. 요청이 들어오면 JwtAuthFilter 를 거치며 인증/인가가 수행된다.
  3. 로그아웃 API 에서 로그아웃 비즈니스 로직이 수행된다.
    • Redis 에 저장되어 있는 Refresh Token 제거
    • AccessToken Black List 등록

로그아웃 구현

로그아웃 API

@RequiredArgsConstructor
@RequestMapping("/user")
@RestController
public class UserAuthAPI {

    private final UserAuthService userAuthService;

    @PostMapping("/logout")
    public ResponseEntity<?> logout(HttpServletRequest httpServletRequest) {
        userAuthService.logout(httpServletRequest);

        return ResponseEntity.ok().build();
    }
}

UserAuthService 로그아웃

@RequiredArgsConstructor
@Service
public class UserAuthService {

		private final JwtProvider jwtProvider;
    
    private final BlackListRedisRepository blackListRedisRepository;
    private final RefreshTokenRedisRepository refreshTokenRedisRepository;

		... 
		
    public void logout(HttpServletRequest httpServletRequest) {

        String resolvedToken = (String)httpServletRequest.getAttribute("resolvedToken");

        // 1. Access Token 에서 User email 을 가져옵니다.
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        // 2. Redis 에서 해당 User email 로 저장된 Refresh Token 이 있는지 여부를 확인 후 있을 경우 삭제합니다.
        refreshTokenRedisRepository.deleteById(authentication.getName());
        // 3. Redis 에서 해당 Access Token 을 Black List 로 저장합니다.
        blackListRedisRepository.save(BlackList.builder()
                .id(authentication.getName())
                .accessToken(resolvedToken)
                .expiration(jwtProvider.getExpiration(resolvedToken))
                .build());

    }
}

로그아웃 된 AccessToken 을 이용한 요청 반려시키기

  • JwtProvider - validateToken(String)
    → JWT 을 검증합니다.
    → Type이 AccessToken 일때, BlackList 에 포함되어있는지 확인한다.
    → 포함되어 있다면, INVALID_ACCESS_TOKEN 예외를 던진다.
@Component
public class JwtProvider {

		private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String BEARER_TYPE = "Bearer";
    private static final String AUTHORITY_KEY = "authority";
    private static final String TYPE_KEY = "type";    
    ...
    
    private final Key key;

    public JwtProvider(@Value("${jwt.secret.key}") String secretKey) {...}

    ...
    
    /**
     * JWT 검증 수행
     */
    public boolean validateToken(String token) {
        try {
            Claims claims = parseClaims(token);
            List<String> authorityList = claims.get(AUTHORITY_KEY, List.class);
            // 권한이 비어있다면, 예외 반환
            if (authorityList.isEmpty()) {
                throw new CustomCommonException(UserErrorCode.UNAUTHORIZED_JWT);
            }
            
            // access token 이 black list 에 저장되어 있는지 확인
            if (getType(token).equals(TYPE_ACCESS) && blackListRedisRepository.findByAccessToken(token).isPresent()) {
                throw new CustomCommonException(UserErrorCode.INVALID_ACCESS_TOKEN);
            }
            
            return true;
        } catch (ExpiredJwtException e) {
		        // 유효기간 만료
            throw new CustomCommonException(UserErrorCode.EXPIRED_JWT);
        } catch (UnsupportedJwtException e) {
		        // 미지원 JWT
            throw new CustomCommonException(UserErrorCode.UNSUPPORTED_JWT);
        } catch (CustomCommonException e) {
		        // try 문 내부에서 반환되는 예외
            throw e;
        } catch (Exception e) {
		        // 기타 JWT 예외
            throw new CustomCommonException(UserErrorCode.INVALID_JWT);
        }
    }
    
    ...
    
}
profile
소통을 중요하게 여기며, 정보의 공유를 통해 완전한 학습을 이루어 냅니다.

0개의 댓글