로그아웃 시, AccessToken → BlackList 등록
- 로그아웃 시, 해당 사용자의 AccessToken 은 더 이상 사용하면 안된다.
- BlackList 로 관리해주며, 해당 AccessToken 을 사용하여 요청할 시, Access 를 반려시킨다.
- BlackList 는 In-Memory Database 인 Redis 를 이용하여 구현하였다.
로그아웃 처리 순서
- 클라이언트에서
/user/logout
(서버 로그아웃 API)으로 Http 헤더에 에 AccessToken 을 넣어서 요청한다.
- 요청이 들어오면 JwtAuthFilter 를 거치며 인증/인가가 수행된다.
- 로그아웃 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");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
refreshTokenRedisRepository.deleteById(authentication.getName());
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) {...}
...
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);
}
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) {
throw new CustomCommonException(UserErrorCode.UNSUPPORTED_JWT);
} catch (CustomCommonException e) {
throw e;
} catch (Exception e) {
throw new CustomCommonException(UserErrorCode.INVALID_JWT);
}
}
...
}