📘JWT 사용하여 로그아웃 기능 구현
JWT 로그인 기능 구현
[로그아웃 흐름]
1. 헤더와 쿠키에서 토큰 가져옴
2. Access Token 블랙리스트 DB 등록
3. 블랙리스트 등록된 Access Token 은 유효성 소멸
4. Refresh Token DB 삭제 및 쿠키에서 리셋
@Getter
@Entity
@Table(name = "tokenBlackList")
public class TokenBlackList{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String accessToken;
public TokenBlackList() {
}
public TokenBlackList(String accessToken) {
this.accessToken = accessToken;
}
}
Access Token 을 담을 블랙리스트 Entity 생성@PostMapping("/logout")
public ResponseEntity<Void> logout(HttpServletRequest request,
HttpServletResponse response){
//Cookie와 Header 에서 Token 가져오기
String accessToken = tokenExtractor.extractAccessTokenFromHeader(request).orElse(null);
String refreshToken = tokenExtractor.extractRefreshTokenFromCookie(request).orElse(null);
authService.logout(new TokenDto(accessToken,refreshToken));
//쿠키에서 refreshToken 리셋
tokenCookieUtils.deleteRefreshTokenCookie(response);
return new ResponseEntity<>(HttpStatus.OK);
}
// Header 에서 access token 값을 가져온다.
public Optional<String> extractAccessTokenFromHeader(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader("Authorization")) // Authorization 헤더의 값을 Optional로 감싼다.
.filter(header -> header.startsWith("Bearer ")) // 값이 "Bearer "로 시작하는지 확인 - Bearer : 토큰 종류
.map(header -> header.substring(7)); // "Bearer " 길이만큼 잘라서 리턴
}
// Cookie 에서 refresh token 값을 가져온다.
public Optional<String> extractRefreshTokenFromCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies == null) return Optional.empty();
//쿠키에서 토큰 찾아서 유효성 검증 후 리턴
return getRefreshTokenFromCookie(cookies);
}
//쿠키에서 refresh_token 이름으로 토큰 받기
private Optional<String> getRefreshTokenFromCookie(Cookie[] cookies){
for (Cookie cookie : cookies) {
if ("refresh_token".equals(cookie.getName())) {
String refreshToken = cookie.getValue();
if(jwtTokenProvider.validateToken(refreshToken)){
return Optional.ofNullable(cookie.getValue());
}
}
}
return Optional.empty();
}
//토큰 유효성 검증(key, 만료, 유효 시작, 형식)
//보안상 예외 처리는 최소화
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
Null 방지를 위해 Optional 타입으로 리턴해준다.//로그아웃 기능
public void logout(TokenDto tokenDto) {
//accessToken 블랙리스트 추가 -> 이미 추가되어 있거나 null 일 경우 예외 처리(비정상 접근)
if(!tokenBlacklistService.isAccessTokenBlackListOrSave(tokenDto.getAccessToken())) {
throw new CustomException(ErrorCode.INVALID_ACCESS);
}
//토큰 있을 시 실행
if(tokenDto.getRefreshToken() == null) {
throw new CustomException(ErrorCode.INVALID_ACCESS);
}
//DB 에서 refreshToken 삭제
tokenService.deleteRefreshTokenDB(tokenDto.getRefreshToken());
}
//Access 토큰 블랙리스트 등록
public boolean isAccessTokenBlackListOrSave(String accessToken) {
if(accessToken == null || tokenBlackListRepository.existsByAccessToken(accessToken)) return false;
tokenBlackListRepository.save(new TokenBlackList(accessToken));
return true;
}
//클라이언트 쿠키에서 삭제 (MaxAge = 0)
public void deleteRefreshTokenCookie(HttpServletResponse response) {
Cookie cookie = new Cookie("refresh_token", null);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(0);
response.addCookie(cookie);
}

200 OK 가 나오게 된다.