Security와 JWT를 연동하고, Global Exception을 처리하며 학습했던 내용을 정리한 글입니다.
Spring Security
Spring Security는 Spring 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크입니다.
'인증'과 '권한'을 Filter 라는 것으로 처리합니다. 단어의 뜻 그대로 필터링 한다고 생각하면 좋을 것 같습니다.
Security with JWT
Spring Security 에서는 Session 방식 뿐만 아니라 JWT도 지원합니다.
기존 5.7 미만 버전에서는 WebSecurityConfigurerAdapter 를 상속받아서 사용했는데 deprecated 되었습니다.
이후 버전 부터는 아래 방식 처럼 사용합니다.
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JwtUtil jwtUtil;
private final UserDetailsManager manager;
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http
) throws Exception{
http
.csrf(AbstractHttpConfigurer::disable)
.addFilterBefore(new JwtTokenFilter(jwtUtil,manager), AuthorizationFilter.class)
.addFilterBefore(new ExceptionHandlerFilter(), JwtTokenFilter.class);
return http.build();
}
}
Jwt를 연동하고 난 후 비즈니스 로직을 작성하던 중, BaseResponse 객체의 필요성을 느껴 이를 정의했습니다.
Controller 에서 나가는 모든 응답은 해당 객체 형태로 가공되어 나갑니다.
제네릭을 사용하여 동적으로 데이터를 추가할 수 있도록 하였습니다.
@Getter
@Setter
@ToString
@Builder
public class BaseResponseDto<D> {
private static final ObjectMapper objectMapper = new ObjectMapper();
private final int status;
private final String message;
private final D data;
private final String path;
private final String error;
private final String timestamp;
public String convertToJson() throws JsonProcessingException {
return objectMapper.writeValueAsString(this);
}
}
이후 JWT 만료, 재발급 관련 기능을 구현하던 중 Filter 단에서 처리하는 에러가 사전에 정의해둔 BaseResponse와 구조가 다른 것을 발견했습니다. Security에서 알아서 처리해준다고 생각하고 있다가 놓쳤던 부분입니다.
일관성있는 응답을 위해 BaseResponse 객체를 정의해두었지만 Filter 단에서 발생한 에러는 BaseResponse로 처리하지 않고 있었던 것 입니다.
이를 처리하기 위해 @RestControllerAdvice 를 사용했습니다.
하지만 이는 Controller 단에 도달한 이후 부터 Exception을 처리하는 방식이였고, 실제 처리가 필요한 Filter단에서 발생한 Exception은 전혀 잡아내지 못하고 있었습니다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CustomException.class)
public ResponseEntity<BaseResponseDto<Object>> handleCustomException(CustomException ex, HttpServletRequest req) {
GlobalException globalException = ex.getGlobalException();
BaseResponseDto<Object> response = BaseResponseDto.builder()
.status(globalException.getStatus())
.message(globalException.getMessage())
.path(req.getServletPath())
.data(null)
.timestamp(LocalDateTime.now().toString()
).build();
return new ResponseEntity<>(response, HttpStatus.valueOf(globalException.getStatus()));
}
}
그래서 ExceptionHandlerFilter 라는 새로운 Filter를 만들었고, JwtFilter 이후 ExceptionHandlerFilter를 배치하여 순차적으로
에러를 처리할 수 있도록 하였습니다.
@Component
public class ExceptionHandlerFilter extends OncePerRequestFilter {
@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 {
String errorCode = "";
if (ex instanceof CustomJwtException) {
errorCode = ((CustomJwtException) ex).getCode();
}
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write(
BaseResponseDto.builder()
.status(status.value())
.error(errorCode)
.message(ex.getMessage())
.path(request.getServletPath())
.timestamp(LocalDateTime.now().toString())
.build()
.convertToJson()
);
}
}
Filter 단에서 발생한 Exception은 Dispatcher Servlet에 도달하기도 전에 Filter단에서 처리가 됩니다.
그래서 애플리케이션 단에서 글로벌하게 처리하던 로직이 전혀 먹히지 않습니다.
이러한 이유로 Filter단에서 발생한 Exception은 새로운 Filter로 처리하는 방식을 취해야 합니다.
위 ExceptionHandlerFilter 가 그 역할을 수행하고 있습니다.
마지막부분을 보면 BaseResponseDto를 재사용 하는 모습을 볼 수 있습니다.
이전에 정의해둔 Dto를 다시 사용하기 위해 이런 방식으로 구현했고, 덕분에 일관성 있는 응답 객체를 전송할 수 있게 되었습니다.
'⚙️ BE' 카테고리의 다른 글
csrf (0) | 2024.04.09 |
---|---|
XSS ( Cross-site scripting ) ( 1 ) (0) | 2024.04.09 |
JWT 관리와 탈취대응 시나리오 ( 2 ) (1) | 2024.04.09 |
JWT 관리와 탈취대응 시나리오 ( 1 ) (0) | 2024.04.09 |
Spring Boot Profile (0) | 2024.04.09 |