빙응의 공부 블로그
[UniP]JWT 검증 필터 예외처리는 어떻게 처리할까? 본문
📝 1. 문제정의
JWT 구현 중에 문제가 생겼다. 왜 JwtFilter에 대한 예외처리가 안먹지??
📌 기존의 JwtFilter
- JWT를 진행하면서 필터를 통해 유효성 검사를 진행하였다.
- 검사에 맞지 않으면 ExpiredJwtException가 발생해서 ExcetionHandler를 통해 잡을 예정이였다.
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 요청 URL을 가져와서 로깅
String requestUri = request.getRequestURI();
if (requestUri.matches("^\\/login(?:\\/.*)?$")) {
filterChain.doFilter(request, response);
return;
}
if (requestUri.matches("^\\/oauth2(?:\\/.*)?$")) {
filterChain.doFilter(request, response);
return;
}
String accessToken = request.getHeader("access");
// AccessToken이 없거나, 만료된 경우
if (accessToken == null) {
filterChain.doFilter(request, response);
return;
}
if (jwtUtil.isExpired(accessToken)) {
filterChain.doFilter(request, response);
return;
}
// 카테고리가 Access인지 확인
if (!"access".equals(jwtUtil.getCategory(accessToken))) {
log.warn("Invalid token category");
filterChain.doFilter(request, response);
return;
}
// 유효한 JWT 토큰이 존재하는 경우
String token = accessToken;
// JWT 토큰에서 사용자 정보를 추출하여 MemberDTO를 생성
MemberDTO memberDTO = MemberDTO.builder()
.username(jwtUtil.getUsername(token))
.role(jwtUtil.getRole(token))
.build();
// CustomOAuth2User 객체를 생성하여 사용자 정보를 설정
CustomUserDetails customOAuth2User = new CustomUserDetails(memberDTO);
// 사용자 정보를 기반으로 Spring Security의 Authentication 객체를 생성
Authentication authToken = new UsernamePasswordAuthenticationToken(
customOAuth2User,
null,
customOAuth2User.getAuthorities()
);
// SecurityContext에 Authentication을 설정하여 현재 사용자를 인증 상태로 설정
SecurityContextHolder.getContext().setAuthentication(authToken);
log.info("User authenticated: {}", memberDTO.getUsername());
filterChain.doFilter(request, response);
}
}
📌필터의 jwt 인증 실패 Exception을 받는 Handler
@ExceptionHandler(ExpiredJwtException.class)
@ResponseBody
public ResponseEntity<String> handleExpiredJwtException(ExpiredJwtException ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("JWT Token has expired");
}
📝 2. 원인 추론
사실 해당 문제의 답은 해커톤 진행 중에
다른 팀 코드를 리뷰해보면서 알게 되었다...
바로 JwtFilter는 Spring Security의 필터라는 것이다.
📌현재 @ControllerAdvice의 문제점
현재 문제는 JwtException이 발생했을 때 @ControllerAdvice의 전역 예외 처리기가 이를 처리하지 못하고 있다는 점이다.
📌@ControllerAdvice와 JwtFilter의 범위 차이
- JwtFilter에서 나오는 JwtException이 Spring Security 필터 단계에서 발생된다는 것을 알았다.
- 그렇기에 ControllerAdvice는 컨트롤러에서 발생한 예외만 처리할 수 있어, 필터 단계에서 발생한 예외를 포착하지 못하는 것이다.
즉!
JwtFilter의 Exception은 Security 단계에서만 발생하기 때문에 Handler가 잡지 못하는 것이다.
📝 3. 해결방안
- 해결방안은 스택오버플로에서 찾을 수 있었습니다.
- java - Handling JWT Exception in Spring MVC - Stack Overflow
Handling JWT Exception in Spring MVC
I am trying to implement Token Authentication on our REST Api, and currently I am referring to this article. On the article it discusses that on creating the token JWT was used, but my current prob...
stackoverflow.com
📌 1. AuthenticationEntryPoint를 통한 인증 실패 처리
- AuthenticationEntryPoint는 Spring Security에서 인증이 필요한 요청을 처리하기 위한 인터페이스이다.
- 다음과 같은 역할을 한다.
- 인증 실패 처리 : 인증이 유효하지 않을 경우 AuthenticationEntryPoint를 통해 응답을 반환
- 클라이언트에게 적절한 피드백 제공 : 인증이 실패했을 때, 클라이언트에게 인증이 필요하다는 것을 명확히 알린다.
📌 2. 직접 JWT 검증 및 응답 설정
- AuthenticationEntryPoint는 인증 실패에 대한 예외처리를 합니다.
- 그렇기에 JWT 검증 과정에서 발생하는 오류를 처리하기 위해 필터에서도 직접 예외를 던지는 것이 좋습니다.
📝 4. 문제 해결
JwtExceptionFilter
- 이 필터는 요청 처리 중 발생하는 JwtException을 포착하여 적절한 에러 응답을 설정합니다.
- 중요한 점은 Jwt 토큰 유효성 검사 전에 나와 chain.doFilter(request, response) 호출 후 예외가 발생하면 setErrorResponse 메서드를 호출하여 JSON 형식으로 응답을 작성합니다.
@Component
public class JwtExceptionFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
try {
chain.doFilter(request, response); // go to 'JwtAuthorizationFilter'
} catch (JwtException exception) {
setErrorResponse(HttpStatus.UNAUTHORIZED, response, exception);
}
}
public void setErrorResponse(HttpStatus status, HttpServletResponse res, Throwable ex) throws IOException {
res.setStatus(status.value());
res.setContentType("application/json; charset=UTF-8");
ObjectMapper objectMapper = new ObjectMapper();
String jsonResponse = objectMapper.writeValueAsString(ResponseDto.fail(HttpStatus.UNAUTHORIZED.value(), ex.getMessage()));
res.getWriter().write(jsonResponse);
}
}
JwtAuthorizationFilter
- 이 필터는 JWT를 검증하고 사용자 인증을 수행합니다.
- 유효한 JWT가 있는 경우 사용자 정보를 로드하여 SecurityContextHolder에 인증 정보를 설정합니다.
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final CustomUserDetailsService userDetailsService;
private static final String AUTHORIZATION_HEADER = "access";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String accessToken = resolveToken(request);
if (StringUtils.hasText(accessToken) && jwtUtil.validateToken(accessToken)) {
String username = jwtUtil.getUsername(accessToken);
CustomUserDetails customUserDetails = userDetailsService.loadUserByUsername(username);
Authentication authToken = new UsernamePasswordAuthenticationToken(
customUserDetails, null, customUserDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authToken);
log.info("User authenticated: {}", username);
}
filterChain.doFilter(request, response); // 필터 체인 계속 진행
}
private String resolveToken(HttpServletRequest request) {
// "access" 헤더에서 토큰 추출
return request.getHeader(AUTHORIZATION_HEADER);
}
}
JwtAuthenticationEntryPoint
- 인증이 실패할 경우 사용됩니다. 클라이언트에게 401 Unauthorized 응답을 반환합니다.
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
📝 결론
Spring Security의 필터에서 나오는 오류는 Handler로 잡을 수 없다.
- 그렇기에 AuthenticationEntryPoint 이용하여 인증 실패를 잡아야 한다.
- 또한 필터로 직접 예외를 던져 추가적인 검증도 진행하는게 좋다.
조합하여 사용:
- 필터: JWT 검증 및 예외 발생 처리.
- AuthenticationEntryPoint: 인증 실패 시 일관된 응답 제공.
✔ 이렇게하면 장점이 뭔데?
- 일관된 예외 처리: AuthenticationEntryPoint를 통해 모든 인증 실패를 일관되게 처리할 수 있다.
- 유연한 오류 응답: 필터에서 직접 예외를 처리하여 클라이언트에게 필요한 정보를 제공할 수 있다.
- 클린한 코드: 각 구성 요소의 책임이 명확해져 코드 유지 보수가 용이하다.
'Project > UniP' 카테고리의 다른 글
[UniP] Real MySQL로 배우는 무한 페이징 & 복합 인덱스 설계 전략 (0) | 2025.01.01 |
---|---|
[UniP]무한 페이징 기능 구현 및 성능 개선 (0) | 2024.11.16 |
[UniP] 인증 메일 발송 비동기 처리하기 (1) | 2024.11.11 |
[UniP]QueryDSL로 조회 최적화하기 (0) | 2024.11.07 |
[UniP]Redis를 통한 RefreshToken 관리하기 (0) | 2024.11.06 |