HandlerInterceptor & Filter / SecurityFilterChain
1. HandlerInterceptor & Filter
- 핵심 기능을 실행하는데 반드시 필요하지는 않지만, 여러 핵심기능에 걸쳐서 동일하게 작동하는 기능이 존재할 수 있다. (ex. 한 메서드가 실제로 실행하는데 얼마나 시간이 걸렸는지 확인하는 메서드.)
- 핵심 기능 관리 코드는 건드리지 않으면서 여러 핵심 기능들이 활용할 수 있는 기능! >> 요청과 응답의 전후로 코드를 실행할 수 있는 방법 >> 이것이 바로 HandlerInterceptor & Filter
1) HandlerInterceptor : Spring Framework의 일부분.
- DispatcherServlet이 HandlerMethod로 요청을 넘기기 전에 실행. = 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나, 가공할 수 있는 기능을 제공.
- 비즈니스 로직과 연관된 기능 구현에 사용.
2) Filter : Jakarta Servlet API의 일부분.
- DispatcherServlet에 도달하기 전에 요청을 검사.
- Spring 외부의 기능이므로 > 예외처리 등 Spring Framework의 도움을 받지 못한다. (Web Container에 의해 관리됨)
- 비즈니스 로직과 무관한 기능 구현
1) HandlerInterceptor class
- 디스패처 서블릿이 HandlerMapping을 통해 적절한 컨트롤러를 찾는다. >> 이 결과로 실행 체인(HandlerExecutionChain)을 반환한다.
*디스패처 서블릿 관련한 글은 👇여기서 정리했다.
- 무튼 이 HandlerExecutionChain은 인터셉터가 등록되어 있으면 > 순차적으로 인터셉터들을 거쳐 컨트롤러가 실행되도록 하고,
없다면 > 바로 컨트롤러를 실행한다.
** HandlerInterceptor 인터페이스의 메서드 3가지
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler
) throws Exception {
return true;
}
default void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
@Nullable ModelAndView modelAndView
) throws Exception {}
default void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
@Nullable Exception ex
) throws Exception {}
}
- preHandle : HandlerMethod 실행 전(즉 컨트롤러가 호출되기 전) 실행되는 메서드
- preHandle에서 false를 반환하면 요청이 전달되지 않는다.
- handler 파라미터 : HandlerMapping이 찾아준 컨트롤러 빈에 매핑되는 HandlerMethod라는 새로운 타입의 객체이다. (@RequestMapping이 붙은 메서드의 정보를 추상화한 객체)
- postHandle : HandlerMethod 실행 후(즉 컨트롤러가 호출된 후), 응답이 전달되기 전 실행되는 메서드.
- 추가로 View를 전달한다면 View의 내부를 채워넣기 전에 실행되는 메서드. (ModelAndView타입의 정보)
- 컨트롤러 하위 계층에서 작업을 진행하다가 중간에 예외가 발생하면 postHandle은 호출되지 않는다.
- afterCompletion : 요청의 처리가 마무리된 뒤에 실행되는 메서드.
- postHandle과 달리, 예외가 발생했다면 예외의 정보가 인자로 추가됨.
- 그래서 예외가 발생하더라도 afterCompletion은 반드시 호출된다.
ex) 어떤 header가 있었는지를 로그로 남기는 인터셉터 만들어보기
1) HandlerInterceptor를 implements하는 인터셉터 만들기.
@Component
@Slf4j
public class HeaderLoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler
) throws Exception {
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
2) preHandle
: HandlerMethod는 메서드와 빈을 포함해 handler method에 대한 정보를 캡슐화하는 객체이다! 얘로 getMethod의 getName을 하여 어떤 요청인지 로깅 가능.
// HeaderLoggingInterceptor class
@Override
public boolean preHandle(HttpServletRequest request,//요청
HttpServletResponse response, //응답
Object handler //실제로 요청을 처리할 RequestMapping을 나타내는 메서드 객체
) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler; //Object handler 객체를 HandlerMethod로 형변환
//어떤 요청인지 로깅하기
log.info("pre handling of {}", handlerMethod.getMethod().getName());
// request의 headerName들을 받고 그 headerName으로 header를 받아서 로깅하기
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
log.info("{}: {}", headerName, request.getHeader(headerName));
}
//여기까지 preHandle이라고 로깅하기.
log.info("=========================================end of pre handling");
// false를 반환하면 요청이 전달되지 않으므로 true~
return true;
}
2) postHandle
// HeaderLoggingInterceptor class
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView
) throws Exception {
//Object handler 객체를 HandlerMethod로 형변환
HandlerMethod handlerMethod = (HandlerMethod) handler;
//어떤 요청인지 로깅하기
log.info("post handling of {}", handlerMethod.getMethod().getName());
//response의 headerNames를 받고 그 headerName으로 header 받아서 로깅하기
Collection<String> headerNames = response.getHeaderNames();
for (String headerName : headerNames) {
log.info("{}: {}", headerName, request.getHeader(headerName));
}
//여기까지 postHandle이라고 로깅하기
log.info("=========================================end of post handling");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
3) afterCompletion
// HeaderLoggingInterceptor class
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
log.info("afterCompletion :: Bye!");
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
4) Configuration를 통해 실행하도록 설정한다.
: WebMvcConfigurer을 상속받아 addInterceptors 메서드를 오버라이딩한다. > 여기서 interceptor를 등록
// InterceptorConfig class
@Configuration
@RequiredArgsConstructor
public class InterceptorConfig implements WebMvcConfigurer {
private final HeaderLoggingInterceptor loggingInterceptor;
@Override
// 인터셉터를 등록하기 위한 메서드.
public void addInterceptors(InterceptorRegistry registry) {
// 어떤 인터셉터를
registry.addInterceptor(loggingInterceptor)
//어떤 경로에
.addPathPatterns("/tests");
}
}
5) Controller는 이런 모습이다.
@RestController
@Slf4j
@RequestMapping("/tests")
public class TestController {
@GetMapping
public String test() {
return "done";
}
@PostMapping
public String testBody(
@RequestBody
TestDto dto
) {
log.info(dto.toString());
return "dooooooone";
}
}
결과는 이렇게 나온다~ preHandle 부터 postHandle, afterCompletion까지~~~!! 신기하네
2) Filter class
- Filter(jakarta.servlet) 인터페이스 구현 (import시 주의하기)
- Filter 인터페이스를 구현한 필터를 만들면 된다. SpringBoot는 Filter 인터페이스를 구현하고, Bean 객체로 등록하면 Filter를 자동으로 사용하게 된다.
- Servlet API를 바로 활용하기 때문에 내부에서 ServletRequest나 ServletResponse를 직접 조작 가능 (그러나 Spring에서 제공하는 편의 기능을 활용하기 어렵다)
- doFilter() 오버라이딩
** Filter interface의 3가지 메서드
package jakarta.servlet;
import java.io.IOException;
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {
}
void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException;
default void destroy() {
}
}
- init : 필터 객체를 초기화하고 서비스에 추가하기 위한 메서드. 웹 컨테이너가 1회 init 메서드를 호출하여 필터 객체를 초기화하면 이후의 요청들은 doFilter를 통해 처리된다.
- doFilter : url-pattern에 맞는 모든 HTTP 요청이 > 디스패처 서블릿으로 전달되기 전 > 웹 컨테이너에 의해 실행되는 메서드.
- 파라미터로 FilterChain이 있는데, FilterChain의 doFilter를 통해 다음 대상으로 요청을 전달한다!! > 원하는 처리를 원하는 순간에 해줄 수 있다.
- destroy : 필터 객체를 서비스에서 제거하고, 사용하는 자원을 반환하기 위한 메서드. 웹 컨테이너가 1회 호출하며 이후에는 doFilter에 의해 처리되지 않는다.
ex. 요청과 응답을 로깅하는 Filter
- @Component를 해주면 모든 url에 적용
- @WebFilter(urlPatterns = <url>) + @ServletComponentScan :: 순서 지정이 어렵다
- FilterRegistrationBean으로 등록하기 (가장 옵션이 많다)
package com.example.auth.filters;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
public class LoggingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
// ServletRequest 요청을 HttpServletRequest로 형변환
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// request 시작 : 어떤 HttpMethod로 요청이 왔는지, 어떤 URI로 요청이 왔는지 로깅하기
log.info("start requests: {} {}",
httpServletRequest.getMethod(),
httpServletRequest.getRequestURI());
// FilterChain의 doFilter로 다음 필터에 체이닝하기 (호출하지 않을 경우 다음 필터가 실행되지 않음)
chain.doFilter(request, response);
// -- 이 아래는 요청 처리 후
// 요청 처리 후에는 Response 보여주기
// ServletResponse 응답을 HttpServletResponse로 형변환
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
log.info("send response : {}", httpServletResponse.getStatus());
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
>> 결과는 이렇다. 맨 위/맨 아래만 Filter logging이다!!
요청의 HTTP Method 잘 나왔고, 어떤 URI로 왔는지 잘 나왔다.
응답의 HTTP Status도 잘 나왔다~
오예!
2. SecurityFilterChain
- Spring Security는 Filter를 이용해 그 기능을 제공한다.
- 어떤 클라이언트의 요청에 대해서 구성된 Filter들을 담고 있는 FilterChain 객체를 통해 순차적으로 사용한다. (FilterChain의 doFilter(request, response)를 통해 연결)
- 기본적인 Servlet Filter는 Spring Application과 별도이기 때문에 > Bean 객체를 찾지 못한다.
>> 그래서 Spring에서는 DelegatingFilterProxy를 활용해 Bean 객체를 찾을 수 있는 Filter를 만들게 해준다.
- Spring Security는 DelegatingFilterProxy를 활용해, FilterChainProxy를 등록한다.
- FilterChainProxy에는 다시 우리가 구성한 SecurityFilterChain이 등록된다.
- 이 SecurityFilterChain에 우리가 만든 Filter를 등록하고, 인증을 진행할 수 있다.
>> 너무 길어져서 이것에 대해서는 더 자세히 다음에 다루겠다!
'Programming > Spring, SpringBoot' 카테고리의 다른 글
Authorization (0) | 2024.01.30 |
---|---|
JWT (1) | 2024.01.29 |
Spring의 Dispatcher Servlet (3) | 2024.01.26 |
CustomUserDetailsManager과 CustomUserDetails 만들기 (1) | 2024.01.26 |
Spring Security... 그리고 인터페이스 (0) | 2024.01.25 |