Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,15 @@ protected Claims validateAndParseToken(final String token) {
}

if (claims.getExpiration().toInstant().isBefore(new Date().toInstant())) {
throw new CustomRuntimeException(ErrorCode.EXPIRED_TOKEN);
throw new ExpiredJwtException(
null,
claims,
"JWT 토큰이 만료되었습니다."
);
}
return claims;
} catch (ExpiredJwtException exception) {
throw exception;
} catch (JwtException | IllegalArgumentException exception) {
throw new CustomRuntimeException(ErrorCode.INVALID_TOKEN_TYPE);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
package life.mosu.mosuserver.global.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import life.mosu.mosuserver.global.exception.CustomRuntimeException;
import life.mosu.mosuserver.global.exception.ErrorResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
@RequiredArgsConstructor
public class TokenExceptionFilter extends OncePerRequestFilter {

private final ObjectMapper objectMapper;

@Override
protected void doFilterInternal(
final HttpServletRequest request,
Expand All @@ -22,11 +29,43 @@ protected void doFilterInternal(
) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (CustomRuntimeException exception) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
} catch (CustomRuntimeException ex) {

ErrorResponse errorResponse = ErrorResponse.builder()
.status(ex.getStatus().value())
.message(ex.getMessage())
.code(ex.getCode())
.build();

response.setStatus(HttpStatus.UNAUTHORIZED.value());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

CustomRuntimeException을 처리할 때 응답 본문의 status 필드에는 ex.getStatus().value()를 사용하면서, 실제 HTTP 응답 상태 코드는 HttpStatus.UNAUTHORIZED.value()로 고정하고 있습니다. 이 두 값은 일치해야 합니다. response.setStatus() 호출 시에도 ex.getStatus().value()를 사용하여 예외에 정의된 정확한 상태 코드가 반환되도록 수정해야 합니다.

Suggested change
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setStatus(ex.getStatus().value());

response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");

objectMapper.writeValue(response.getWriter(), errorResponse);
} catch (ExpiredJwtException ex) {
ErrorResponse errorResponse = ErrorResponse.builder()
.status(HttpStatus.NOT_ACCEPTABLE.value())
.message("토큰이 만료되었습니다.")
.code("TOKEN_EXPIRED")
.build();

response.setStatus(HttpStatus.NOT_ACCEPTABLE.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.sendError(exception.getStatus().value(), exception.getMessage());

objectMapper.writeValue(response.getWriter(), errorResponse);
Comment on lines +46 to +56

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

만료된 토큰에 대해 HttpStatus.NOT_ACCEPTABLE (406)을 사용하고 있는데, 이는 일반적으로 Accept 헤더와 서버가 생성할 수 있는 컨텐츠 타입이 맞지 않을 때 사용됩니다. 토큰이 만료된 경우에는 인증이 필요하다는 의미이므로 HttpStatus.UNAUTHORIZED (401)을 사용하는 것이 더 표준에 부합하고 일반적인 REST API 설계 관례에 맞습니다. 클라이언트는 401 응답을 받으면 토큰을 재발급 받거나 다시 로그인하도록 처리할 수 있습니다.

Suggested change
ErrorResponse errorResponse = ErrorResponse.builder()
.status(HttpStatus.NOT_ACCEPTABLE.value())
.message("토큰이 만료되었습니다.")
.code("TOKEN_EXPIRED")
.build();
response.setStatus(HttpStatus.NOT_ACCEPTABLE.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.sendError(exception.getStatus().value(), exception.getMessage());
objectMapper.writeValue(response.getWriter(), errorResponse);
ErrorResponse errorResponse = ErrorResponse.builder()
.status(HttpStatus.UNAUTHORIZED.value())
.message("토큰이 만료되었습니다.")
.code("TOKEN_EXPIRED")
.build();
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
objectMapper.writeValue(response.getWriter(), errorResponse);

} catch (Exception ex) {
ErrorResponse errorResponse = ErrorResponse.builder()
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.message("서버 오류가 발생했습니다.")
.code("INTERNAL_SERVER_ERROR")
.build();

response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");

objectMapper.writeValue(response.getWriter(), errorResponse);
}
Comment on lines +32 to 69

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

catch 블록에서 HttpServletResponse에 에러 응답을 설정하는 코드가 중복되고 있습니다. 이 로직을 별도의 private 메서드로 추출하여 코드 중복을 줄이고 가독성과 유지보수성을 높이는 것을 제안합니다. 예를 들어, 다음과 같은 헬퍼 메서드를 만들 수 있습니다:

private void sendErrorResponse(HttpServletResponse response, ErrorResponse errorResponse) throws IOException {
    response.setStatus(errorResponse.getStatus());
    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    response.setCharacterEncoding("UTF-8");
    objectMapper.writeValue(response.getWriter(), errorResponse);
}

이렇게 하면 각 catch 블록이 더 간결해집니다.

}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package life.mosu.mosuserver.global.filter;

import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -105,9 +106,12 @@ protected void doFilterInternal(

try {
setAuthentication(accessToken);
} catch (ExpiredJwtException e) {
log.error("액세스 토큰 만료: {}", e.getMessage());
throw e;
} catch (CustomRuntimeException e) {
log.error("액세스 토큰 인증 실패: {}", e.getMessage());
throw new CustomRuntimeException(ErrorCode.INVALID_TOKEN);
log.error("유효하지 않은 토큰 인증 실패: {}", e.getMessage());
throw e;
} catch (Exception e) {
log.error("액세스 토큰 인증 실패: {}", e.getMessage());
throw new RuntimeException("액세스 토큰 인증 중 예외 발생", e);
Expand All @@ -120,7 +124,7 @@ private void reissueToken(HttpServletRequest request, HttpServletResponse respon
throws IOException {
final TokenCookies tokenCookies = tokenResolver.resolveTokens(request);
if (!tokenCookies.availableReissue()) {
throw new CustomRuntimeException(ErrorCode.EXPIRED_REFRESH_TOKEN);
throw new CustomRuntimeException(ErrorCode.NOT_FOUND_TOKEN);
}

Token newToken = authTokenManager.reissueToken(tokenCookies);
Expand Down