토큰 로그인을 진행하면 JWT 오류들에 대해 예외 처리 시 @ControllerDevice로 처리가 되지 않는다. 따라서 따로 예외처리가 되지 않고 그대로 예외를 반환하게 되기 때문에, 이의 원인이 무엇이고 어떻게 해결하면 좋을지 고민해보자!

(화살표를 못그렸는데 스프링컨텍스트 내 순서는 dispatcher servlet → interceptor → AOP → Controller → AOP → Interceptor → dipatcher servlet 과 같은 순서다 😅)
먼저 스프링이 요청을 처리하고 응답하는 구조를 보자.
client에서 요청이 들어오면, Web Context에서 filter를 거치게 된다. filter는 스프링 자체의 기능이 아니다.
클라이언트로부터 ServletRequest가 들어오면 Spring Context에 들어와 Dispatcher Servlet에 도달하기 전에 공통된 작업을 처리할 수 있는 기능을 제공한다.
마찬가지로 ServletResponse 가 클라이언트에게 나가기 전에 공통된 작업을 처리할 수 있도록 해준다.
반면 @ControllerAdvice는 애플리케이션 내 모든 컨트롤러에서 발생하는 예외를 처리하는 스프링의 어노테이션이다.
따라서 컨트롤러 내에서 발생한 예외가 아닌 필터에서 발생한 예외는 @ControllerAdvice에서 처리해줄 수 없다는 말이다.
따라서 예외처리를 따로 해주어야하므로, 지금부터 JWTAuthenticationFilter에서 발생한 예외를 잡아 처리해보자!
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String AUTHORIZATION_TYPE = "Bearer ";
private final JwtUtils jwtUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authorization = request.getHeader("Authorization");
//token이 없으면 anonymous User
if (authorization == null) {
filterChain.doFilter(request, response);
return;
}
validateJwtAuthorizationType(authorization);
String jwt = authorization.substring(AUTHORIZATION_TYPE.length());
//token 검증이 완료된 경우에만 authentication을 부여
if (jwtUtils.validateToken(jwt)) {
System.out.println("jwt: " + jwt);
Authentication authentication = jwtUtils.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private void validateJwtAuthorizationType(String authorization) {
if (!authorization.startsWith(AUTHORIZATION_TYPE))
throw new BusinessException(AuthErrorCode.UNSUPPORTED_JWT);
}
}
위와 같이 요청 헤더에서 Authorization 토큰 값을 받아와, 검증을 진행한다. Bearer Token이 아닐 경우, 일관된 예외 처리를 위해 CustomException인 BusinessException에 지정한 ErrorCode를 담아 예외를 반환하도록 하였다.

validationToken에서도, 위와 같이 ErrorCode를 지정하여 CustomException을 반환하도록 지정하였다.

이제 JwtExceptionFilter를 만들어준다. JwtExceptionFilter는 JwtAuthenticationFilter보다 앞단에 위치 시켜 위에서 발생되게 했던 BusinessException을 잡아 응답을 반환하도록 할 것이다.
나는 HandlerUtils에 writeErrorResponse라는 메서드를 만들어 에러 응답을 반환해주게 하였다.

직접 응답을 생성해서 response를 반환해주는 식으로 구성한다. json형태로 응답을 반환할 것이기 때문에 ContentType지정을 해주고, ErrorCode에 담긴 내용들을 담아 응답을 쓰도록 하였다. 나는 json의 순서를 고정시키고 싶어 LinkedHashMap을 사용하여 넣어주었다.
참고로 try문에 괄호를 사용하면 try-with-resources 구문으로 자원 사용이 끝나면 자동으로 자원 반납이된다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder amBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
AuthenticationManager authenticationManager = amBuilder.build();
http.authenticationManager(authenticationManager);
http
.httpBasic(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.disable()
)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/images/**").permitAll()
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/callback/oauth2/code/**").permitAll()
.requestMatchers("/error/**").permitAll()
.anyRequest().authenticated()
)
.....
.addFilterBefore(new JwtExceptionFilter(), JwtAuthenticationFilter.class)
;
SecurityConfig에서 , addFilterBefore를 활용하여 JwtAuthenticationFilter 이전에 JwtExceptionFilter를 넣어준다.
이렇게 코드를 구성하고, 만약 시간이 만료된 토큰을 갖고 인증 요청을 진행하게 된다면
{
"code" : 401,
"status" : "AUTH002",
"message" : "만료된 JWT입니다."
}
와 같이, 우리가 ErrorCode를 커스텀한대로 예외 처리를 진행할 수 있다!
서블릿과 스프링의 구조와, 필터와 스프링 컨테이너간의 관계를 공부하며 이 문제의 원인을 더욱 더 잘 이해할 수 있게 되었던 것 같다. 역시 기본 공부가 정말 중요하다! 스프링아 더 친해지자..👍