반응형
들어가며
Spring Request Life Cycle
- Request : User > Filter > DispatcherServlet > Interceptor > Controller
- Response : Controller > Interceptor > DispatcherServlet > Filter > User
Filter와 Interceptor의 공통점
- 요청에 대한 전, 후처리가 가능
Filter와 Interceptor의 차이
- Filter
- 스프링 컨텍스트 외부에서 실행되므로 빈 사용이 불가능
- 주로 요청 파라미터 자체의 검증 및 처리를 담당
- encoding 처리
- xss 방어 처리 (네이버 lucy-xss-filter 많이 사용하는듯?)
- 예외 발생시 web.xml에서 정의한 페이지로 처리해야함
- Interceptor
- 스프링 컨텍스트 내부에서 실행되므로 빈 사용이 가능함
- 주로 서비스 로직을 활용하여 요청 정보 정합성 처리를 담당
- 로그인 체크
- 권한 체크
- 예외 발생시 ControllerAdvice, ExceptionHandler에서 처리 가능
- 참고
- https://supawer0728.github.io/2018/04/04/spring-filter-interceptor/
- https://goddaehee.tistory.com/154
Filter 예제
설명
- ServletComponentScan + WebFilter 조합으로 구현시 필터 체인의 순서를 정의할 수 없음
- Component + Order 조합으로 구현시 url pattern을 정의할 수 없음
- 고전적인 방법이 제일 좋아보임
RequestLoggingFilter
@Slf4j
public class RequestLoggingFilter extends HttpFilter {
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("logging - before");
chain.doFilter(request, response);
log.info("logging - after");
}
}
AuthFilter
@Slf4j
public class AuthFilter extends HttpFilter {
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("auth - before");
if (StringUtils.equals(request.getRequestURI(), "/api/post")) {
response.sendRedirect("/api/error");
return;
}
// doFilter()를 호출하지 않으면 다음 필터 or 컨트롤러를 수행하지 않음
chain.doFilter(request, response);
log.info("auth- after");
}
}
ApiFilterConfig
@Configuration
public class ApiFilterConfig {
@Bean
public FilterRegistrationBean<RequestLoggingFilter> loggingFilter() {
return apiFilter(new RequestLoggingFilter(), 1);
}
@Bean
public FilterRegistrationBean<AuthFilter> authFilter() {
return apiFilter(new AuthFilter(), 2);
}
private <T extends Filter> FilterRegistrationBean<T> apiFilter(T filter, int order) {
FilterRegistrationBean<T> registrationBean = new FilterRegistrationBean<>(filter);
registrationBean.addUrlPatterns("/api/*");
registrationBean.setOrder(order);
return registrationBean;
}
}
Interceptor 예제
TestInterceptor
public class TestInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
HandlerMethod handlerMethod = (HandlerMethod) handler; // handlerMethod를 통해 컨트롤러 정보 활용 가능
MyAnnotation myAnnotation = handlerMethod.getMethodAnnotation(MyAnnotation.class); // 메소드 어노테이션 활용하는 예시
// Controller 실행 전 동작
// 반환 값이 false일 경우 Controller 진입하지 않음
// Object handler는 Controller 객체
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
// Controller 실행 후 동작
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// View 렌더링 완료 후 실행
}
}
WebMvcConfig
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TestInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login/**");
}
}
Filter에서 Access Log 찍기
Request, Response를 doFilter() 이후에 로그 찍기
@Getter
@Builder
public class Log {
private final String status;
private final String method;
private final String url;
private final Map<String, Object> parameters;
private final Map<String, Object> headers;
private final String body;
public static Log of(HttpServletRequest request, String body) {
return Log.builder()
.status("0")
.method(request.getMethod())
.url(request.getRequestURI())
.parameters(
request.getParameterMap().entrySet().stream()
.collect(Collectors.toMap(
entry -> entry.getKey(),
entry -> getValue(Arrays.asList(entry.getValue())))
)
)
.headers(
Collections.list(request.getHeaderNames()).stream().distinct()
.collect(Collectors.toMap(
name -> name,
name -> getValue(Collections.list(request.getHeaders(name)))
)
)
)
.body(body)
.build();
}
public static Log of(HttpServletResponse response, String body, String method, String url) {
return Log.builder()
.status(String.valueOf(response.getStatus()))
.method(method)
.url(url)
.parameters(Collections.emptyMap())
.headers(
response.getHeaderNames().stream().distinct()
.collect(Collectors.toMap(
name -> name,
name -> getValue(new ArrayList<>(response.getHeaders(name)))
))
)
.body(body)
.build();
}
private static Object getValue(List<String> values) {
return values.size() == 1 ? values.get(0) : values;
}
}
/**
* implementation("commons-io:commons-io:2.15.0")
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class AccessLoggingFilter extends OncePerRequestFilter {
private final ObjectMapper objectMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
// doFilter() 수행된 이후에 getContentAsByteArray()를 호출해야 body 로깅이 가능
filterChain.doFilter(requestWrapper, responseWrapper);
if (log.isInfoEnabled()) {
String requestBody = IOUtils.toString(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8.name());
log.info("[ACCESS_LOG:REQUEST] {}", objectMapper.writeValueAsString(Log.of(request, requestBody)));
String responseBody = IOUtils.toString(responseWrapper.getContentAsByteArray(), StandardCharsets.UTF_8.name());
log.info("[ACCESS_LOG:RESPONSE] {}", objectMapper.writeValueAsString(Log.of(response, responseBody, request.getMethod(), request.getRequestURI())));
}
// 아래 코드를 호출하지 않으면 responseBody가 Empty로 응답
responseWrapper.copyBodyToResponse();
}
}
Request는 doFilter() 호출 전에 찍고 싶을 경우
public class EagerContentCachingRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
@SneakyThrows
public EagerContentCachingRequestWrapper(HttpServletRequest request) {
super(request);
this.body = IOUtils.toByteArray(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() {
return new ServletInputStream() {
private final ByteArrayInputStream in = new ByteArrayInputStream(body);
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException();
}
@Override
public int read() {
return in.read();
}
};
}
}
/**
* implementation("commons-io:commons-io:2.15.0")
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class AccessLoggingFilter extends OncePerRequestFilter {
private final ObjectMapper objectMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.getParameterNames(); // 호출해주지 않으면 application/x-www-form-urlencoded 요청시 body의 정보가 parameter로 변환되지 않음.
EagerContentCachingRequestWrapper requestWrapper = new EagerContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
if (log.isInfoEnabled()) {
String requestBody = IOUtils.toString(requestWrapper.getInputStream(), StandardCharsets.UTF_8);
log.info("[ACCESS_LOG:REQUEST] {}", objectMapper.writeValueAsString(Log.of(request, requestBody)));
}
// doFilter() 수행된 이후에 getContentAsByteArray()를 호출해야 body 로깅이 가능
filterChain.doFilter(requestWrapper, responseWrapper);
if (log.isInfoEnabled()) {
String responseBody = IOUtils.toString(responseWrapper.getContentAsByteArray(), StandardCharsets.UTF_8.name());
log.info("[ACCESS_LOG:RESPONSE] {}", objectMapper.writeValueAsString(Log.of(response, responseBody, request.getMethod(), request.getRequestURI())));
}
// 아래 코드를 호출하지 않으면 responseBody가 Empty로 응답
responseWrapper.copyBodyToResponse();
}
}
참고
- https://cnpnote.tistory.com/entry/SPRING-Spring-Boot-한-곳에서-모든-요청과-응답을-예외로-기록하는-방법
- https://www.sollabs.tech/ContentCachingRequestWrapper
반응형
'Development > Spring' 카테고리의 다른 글
[Spring] Swagger (0) | 2020.12.27 |
---|---|
[Spring] Argument Resolver (0) | 2020.12.27 |
[Spring] Validation (0) | 2020.12.27 |
[Spring] BeanFactory & ApplicationContext (0) | 2020.12.27 |
[Spring] Paging (0) | 2020.12.27 |