😂 ISSUE
엑세스 토큰이 만료된 경우, 토큰이 만료되었다는 예외를 던지는데, 이런 Response가 나온다.
{
"status": "UNAUTHORIZED",
"code": 401,
"message": "인증되지 않은 유저입니다.",
"detail": null
}
인증되지 않은 유저도 맞지만, 난 아래의 형태처럼 더 적절한 예외 메시지를 리턴하고 싶다.
{
"status": "UNAUTHORIZED",
"code": 401,
"message": "토큰이 만료되었습니다.",
"detail": null
}
{
"status": "UNAUTHORIZED",
"code": 401,
"message": "인증용 헤더가 비어있습니다.",
"detail": null
}
{
"status": "UNAUTHORIZED",
"code": 401,
"message": "잘못된 토큰 타입입니다.",
"detail": null
}
🤔 REASON
토큰 만료 예외를 던지려고 한 곳은 필터에서 호출한 토큰 발급기의 토큰 검증 메서드 이다.
// 토큰 발급기
public class JwtTokenProvider {
...
// 토큰 검증 메서드
public boolean validateToken(String jwtToken) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return true;
} catch (IllegalArgumentException e) {
log.error("헤더가 비어있음");
throw new RestApiException(CommonErrorCode.EMPTY_AUTHORIZATION_HEADER, MethodInfoUtil.getDetail());
} catch(ExpiredJwtException e) {
log.error("Token 만료");
throw new RestApiException(CommonErrorCode.EXPIRED_ACCESS_TOKEN, "토큰 만료");
} catch(JwtException e) {
log.error("잘못된 Access Token 타입");
throw new RestApiException(CommonErrorCode.WRONG_TOKEN_TYPE, MethodInfoUtil.getDetail());
}
return false;
}
}
그런데 validateToken() 메서드에서 예외를 던지면 JwtAuthenticationEntryPoint 의 commence() 메서드가 예외를 처리한다.
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
setResponse(response);
}
private void setResponse(HttpServletResponse response) throws IOException {
ErrorResultDto errorResultDto = new ErrorResultDto(HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.value(), "인증되지 않은 유저입니다.");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(errorResultDto));
}
}
저 setResponse() 메서드 때문에 응답 형태가 다음과 같은 것 같다.
{
"status": "UNAUTHORIZED",
"code": 401,
"message": "인증되지 않은 유저입니다.",
"detail": null
}
그럼 Spring Security에서, 토큰을 관리하는 과정에서 JwtAuthenticationEntryPoint 가 어디에 사용되고, 목적이 뭘까?
JwtAuthenticationEntryPoint
인증에 실패한 사용자에 대한 응답을 위해 사용되는데, 내가 구현한 형태는 HttpStatus.UNAUTHORIZED 상태를 응답하도록 되어있다. 주로 401 에러를 처리하기 위해 사용한다. 또한 WebSecurityConfig 를 보면, JwtAuthenticationEntryPoint 는 filterChain() 메서드에서 설정되고 사용된다는 것을 알 수 있다.
public class WebSecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 1. csrf 설정
http.csrf(AbstractHttpConfigurer::disable);
// 2. 권한에 따른 보안 예외 처리 설정
http.exceptionHandling(e -> e.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler));
...
return http.build();
}
}
결국 JwtAuthenticationEntryPoint도 필터에서 사용되는 것이다.
하지만 내가 사용한 공통 예외 처리(@ControllerAdvice)는 디스패처 서블릿 에 붙어있는 기술이다.
이 점을 명심하고 대략적인 사용자 인증의 예외 처리 흐름을 파악해보면 아래와 같다.
공통 예외 처리기인 GlobalExceptionHandler는 사진과 같이 디스패처 서블릿에 붙어있다.
그렇기 때문에 사용자 인증(토큰 인증)과 관련된 예외 처리는 필터에서 해줘야 한다.
😃 SOLVE
일단 JwtAuthenticationEntryPoint 에 있는 setResponse() 메서드는 주석처리 한다.
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// setResponse(response);
}
private void setResponse(HttpServletResponse response) throws IOException {
ErrorResultDto errorResultDto = new ErrorResultDto(HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.value(), "인증되지 않은 유저입니다.");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(errorResultDto));
}
}
그리고 JwtTokenProvider 에서 jwtExceptionHandler() 메서드를 추가한다.
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
private final ObjectMapper objectMapper;
private final UserDetailsService userDetailsService;
...
public boolean validateToken(String jwtToken, HttpServletResponse response) throws IOException {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return true;
} catch (IllegalArgumentException e) {
log.error("헤더가 비어있음");
jwtExceptionHandler(CommonErrorCode.EMPTY_AUTHORIZATION_HEADER, response);
} catch(ExpiredJwtException e) {
log.error("Token 만료");
jwtExceptionHandler(CommonErrorCode.EXPIRED_ACCESS_TOKEN, response);
} catch(JwtException e) {
log.error("잘못된 Access Token 타입");
jwtExceptionHandler(CommonErrorCode.WRONG_TOKEN_TYPE, response);
}
return false;
}
private void jwtExceptionHandler(CommonErrorCode wrongTokenType, HttpServletResponse response) throws IOException {
ErrorResultDto errorResultDto = new ErrorResultDto(wrongTokenType.getHttpStatus(),
wrongTokenType.getHttpStatus().value(),
wrongTokenType.getMessage());
response.setStatus(wrongTokenType.getHttpStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(errorResultDto));
}
}
이렇게 코드를 수정&추가하면 필터에서도 예외 처리를 커스텀할 수 있다.
'ETC > ERROR' 카테고리의 다른 글
[ERROR] M1 맥북 Jmeter 실행 에러 (0) | 2024.08.01 |
---|