[Spring Security] jwt 401 응답 + jwt 예외 핸들러 만들기
일단 security 예외는 사용자가 만든 spring 핸들러에 걸리지 않는다
이유는 security가 spring이 실행되기 전보다 먼저 실행되기 때문이다..
사용자가 만든 핸들러는 spring에 걸려있고 security가 그것보다 더 전에 실행하기 때문에 걸리지 않는다
위 그림처럼 Filter가 Request의 가장 앞단에 위치해있고 HnadlerInterceptor가 controller단에 존재한다
이래서 이 게시글을 작성하기 전까진 jwtFilter에서 발생하는 요런 에러들이 GlobalHandler에 잡히지 않았다
(사실 Spring의 GlobalHandler에 잡히는 줄 알았고 코드 문제로 동작이 안 하는 줄 알았다..._)
그래서 겸사겸사 위와 같은 문제들까지 다 해결하기위해 해당 게시글을 작성하고자 한다!
가장 도움이 많이 된 블로그는 요기! 🙇♂️🙇♂️🙇♂️
전체적인 로직은 위 블로그에서 알려주신대로 위와 같이 진행하려고 한다
즉, 현재 jwtFilter가 걸려있는 쪽보다 더 앞쪽에 Filter를 걸어서 해당 Filter에선 Exception을 체크하고 핸들링해주는 것이다
여기서 A필터는 원래 있던 JwtFilter
가 되는 것이고 B필터는 새롭게 추가해줄 ExceptionHandlerFilter
가 되는 것이다.
즉, ExceptionHandlerFilter
가 먼저❗exception을 체크하고 response에 담아 클라이언트에게 전달해준다
따라서 Filter간의 순서가 중요한데 이때 SecurityConfig
에 해당 필터들의 순서에 유의하여 설정해 주어야한다~! → 3️⃣에서 참고!
1️⃣ JwtFilter 내부 isValidToken 메소드에서 각 jwt 에러에 따라 예외처리 추가
각 jwt 예외 상황마다 new JwtException
으로 커스텀한 예외 메시지를 넣어주고 있다.
참고로 TODO에 적힌 포스트맨 로그인 요청시 jwt 유효성 오류는 시큐리티 설정이 잘못된 줄 알았는데 그게 아니라 포스트맨 문제라고 한다! 그래서 임시 조치로 예외처리를 뺐다
(참고: https://tte-yeong.tistory.com/129)
여기서 중요한 점! ✅
JwtFilter의 doFilterInternal 메소드에서 아래와 같이 예외 처리를 한다면??..
기껏 만든 ExceptionHandlerFilter 가 아니라 JwtFilter에서 예외를 핸들러하는 것이 되므로 JwtFilter에서는 try/catch로 예외 처리를 하지 않도록 하자 (경험담…)
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = resolveToken(request);
try {
if (jwtTokenProvider.isValidToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}catch(JwtException e) {
// 예외 처리 구문..
}
filterChain.doFilter(request, response);
}
// 이러면 jwtFilter에서 예외처리를 하게되어 원하는 결과를 얻을 수 없다! 주의!!
2️⃣ ExceptionHandlerFilter
를 만들자
이제 1️⃣에서 발생하는 JwtException을 처리하기 위한 jwt 예외 핸들러를 만들어보자
즉, 앞서 말한 B필터가 되겠다!
@Slf4j
@Component
public class ExceptionHandlerFilter extends OncePerRequestFilter { // 새롭게 생성!
record ErrorResponse(
String messsage
) {
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (JwtException ex) {
setErrorResponse(HttpStatus.UNAUTHORIZED, request, response, ex);
}
}
public void setErrorResponse(HttpStatus status, HttpServletRequest request,
HttpServletResponse response, Throwable ex) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
response.setStatus(status.value());
response.setContentType("application/json; charset=UTF-8");
ErrorResponse errorResponse = new ErrorResponse(ex.getMessage());
log.error(ex.getMessage());
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}
따라서 jwt 예외가 발생하면 앞서 정의해둔 예외 응답 프로토콜에 따라 응답 객체를 생성하고 ObjectMapper
를 이용하여 클라이언트에게 오류를 Json 형태로 반환하는 로직이다!
3️⃣ SecurityConfig
설정
이제 앞에서 만든(2️⃣) jwt 예외 핸들러를 security에 설정해주는 작업을 하자!
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors().configurationSource(corsConfigurationSource())
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin().disable()
.httpBasic().disable()
.authorizeRequests()
.antMatchers(permitUrls).permitAll()
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(exceptionHandlerFilter, JwtFilter.class); // = exceptionHandlerFilter가 JwtFilter 보다 먼저 실행된다
return http.build();
}
가장 중요한 것은 addFilterBefore 메소드의 인자 순서이다.
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
UsernamePasswordAuthenticationFilter
보다jwtFilter
가 우선이다
.addFilterBefore(exceptionHandlerFilter, JwtFilter.class)
JwtFilter
보다exceptionHandlerFilter
가 우선이다 ⭐⭐⭐- 즉, 위에서 말한 로직대로 B필터가 우선이 되려면 해당 설정이 필수적이다!!
addFilterBefore에 관해 gpt 참고
addFilterBefore 메소드는 Spring Security에서 사용되는 메소드 중 하나로 보안 필터를 특정 필터 이전에 추가하는 데 사용됩니다.
이 메소드는 FilterChainProxy에 의해 호출되며, 보통 Spring Security 설정 클래스에서 사용됩니다.
1. Filter 등록 순서의 중요성
Spring Security는 여러 보안 필터들이 체인 형태로 구성되어 있습니다.
각 필터는 특정한 작업을 수행하며, 필터 체인은 클라이언트의 요청에 대한 처리를 담당합니다.
addFilterBefore 메소드를 사용하여 필터를 특정 필터 이전에 추가함으로써 필터의 실행 순서를 제어할 수 있습니다.
2. 메소드 시그니처
addFilterBefore 메소드는 다음과 같은 시그니처를 가지고 있습니다
void addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter)
- filter: 추가할 필터 객체
- beforeFilter: 추가할 필터가 어떤 필터 이전에 위치해야 하는지를 나타내는 필터 클래스
3. 예시
아래는 addFilterBefore 메소드를 사용한 예시입니다.
이 예시에서는 CustomFilter를 UsernamePasswordAuthenticationFilter 이전에 추가하고 있습니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
**.addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)**
// 다른 설정들...
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
// 다른 설정들...
.and()
.formLogin()
// 다른 설정들...
.and()
.logout()
// 다른 설정들...
.and()
.csrf().disable();
}
}
위의 예시에서 CustomFilter는 UsernamePasswordAuthenticationFilter 이전에 실행되도록 설정되었습니다.
4. 활용 사례
이 메소드는 특정한 필터를 추가하여 사용자 정의 보안 로직을 구현하거나, 기존의 필터를 수정하지 않고 새로운 필터를 추가할 때 유용합니다.
특히, 사용자 정의 로그인, 권한 부여, 혹은 다른 보안 요소를 추가하고 싶을 때 활용될 수 있습니다.
요약하면, addFilterBefore 메소드는 Spring Security에서 필터 체인에 사용자 정의 필터를 특정 필터 이전에 추가하는 데 사용되며, 필터 체인의 동작을 세밀하게 제어할 수 있도록 도와줍니다.
4️⃣ 결과물 ❗
Rerfence
- Spring Security JWT 토큰 검증 시 Exception 예외 처리
- [SPRING] JWT Exception
- [Spring Boot] 나의 필터가 두 번 적용된 이유 - 타깃코더스 -> addFilterBefore 관련 참고글
- 스프링시큐리티 + JWT 로그인 시 토큰 유효성검사 에러 ->jwt 로그인 시 토큰 유효성 검사 에러 관련 참고글