Home Filter, Interceptor
Post
Cancel

Filter, Interceptor

Filter

  • Web Application에서 관리되는 영역으로써 Spring Boot Framework에서 Client로 부터 오는 요청/응답에 대해서 최초/최종 단계의 위치에 존재하며 이를 통해 요청/응답의 정보를 변경하거나, Spring에 의해서 데이터가 변환되기 전의 순수한 Client의 요청/응답 값을 확인 할 수 있다.
  • 유일하게 ServletRequest, ServletResponse의 객체를 변환 할 수 있다.
  • 주로 Spring에서는 Requset/ Response의 Logging용도로 활용하거나, 인증과 관련된 Logic들을 해당 Filter에서 처리한다.
  • 이를 선/후 처리함으로써 Service business logic과 분리 시킨다.

springStructure

Filter 추가

  • filter를 추기하기 위해서는 javax.servlet의 filter interface를 구현해야한다.
  • filter interface에는 3개의 Mehtod가 있다.
    1. init 메소드
    • 초기화 및 서비스에 추가히기위한 메소드이다.
    • 웹 컨테이너가 1회 init 메소드를 호출하여 초기화하고 이후의 요청은 doFilter를 통해 전/후에 처리된다.
  1. doFilter 메소드
    • urlPattern에 맞는 모든 HTTP 요청이 디스패처 서블릿으로 전달되기 전/후에 실행된다.
    • doFilter의 파라미터로인 FilterChain의 doFilter 통해 다음 대상으로 요청을 전달한다.
  2. destroy 메소드
    • 자원 반환 및 서비스에서 제거하기 위한 메소드이다.

filter.GlobalFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Slf4j
@Component
public class GlobalFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //전처리
        HttpServletRequest httpServletRequest = (HttpServletRequest)request;
        HttpServletResponse httpServletResponse = (HttpServletResponse)response;

        String url = httpServletRequest.getRequestURI();

        BufferedReader br = httpServletRequest.getReader();

        br.lines().forEach(line -> {
            log.info("url : {}, line : {}", url, line);
        });
        //BufferedReader를 forEach를 사용해 전부 확인하였기 때문에 seek가 가장 마지막에 가있다.
        //전처리 후 Spring이 body를 가지고 User라는 객체에 Mapping을 시도하는데 이때 읽을 내용이 없기 때문에 Error가 발생한다.


        chain.doFilter(request, response);
        //후처리

    }
}
  • 위의 코드는 주석에 작성된 이유로 Error가 발생한다. 이러한 문제를 해결하기 위해 spring에서는 ContentCachingRequestWrapper, ContentCachingResponseWrapper를 지원한다.
1
2
ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request);
ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);
  • ContentCaching…Wrapper는 처음 캐싱 처리가 될 때 Byte크기만 기록해 두고 내용은 기록되지 않는다. 내용은 해당 request, response가 inputstream으로 읽혀 졌을 때 채워지는데 request는 object mapping 이후에 수행된다. 따라서 이전에 전처리에 작성된 출력을 doFilter이후로 옮겨야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example.filter.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;

@Slf4j
@Component
public class GlobalFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //전처리
        ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request);
        ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);

        chain.doFilter(httpServletRequest, httpServletResponse);
        //후처리

        String url = httpServletRequest.getRequestURI();

        String reqContent = new String(httpServletRequest.getContentAsByteArray());
        log.info("req url : {}, req body : {}", url, reqContent);

        String resContent = new String(httpServletResponse.getContentAsByteArray());

        int httpStatusCode = httpServletResponse.getStatus();
        log.info("res status : {} , res body : {}", httpStatusCode, resContent);

    }
}

test

  • 원하는 Log들이 정상적으로 출력 되었지만 Client는 Body가 비어있는 Response 를 받게 된다. getContentAsByteArray로 response를 다 읽어버렸기 때문인데 copyBodyToResponse를 사용해 다시 body를 채워 넣는것으로 문제를 해결할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Slf4j
@Component
public class GlobalFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //전처리
        ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request);
        ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);

        chain.doFilter(httpServletRequest, httpServletResponse);
        //후처리

        String url = httpServletRequest.getRequestURI();

        String reqContent = new String(httpServletRequest.getContentAsByteArray());
        log.info("req url : {}, req body : {}", url, reqContent);

        String resContent = new String(httpServletResponse.getContentAsByteArray());

        httpServletResponse.copyBodyToResponse();          //이 부분 추가

        int httpStatusCode = httpServletResponse.getStatus();
        log.info("res status : {} , res body : {}", httpStatusCode, resContent);

    }
}

원하는 곳에만 filter적용하기

  • Application에 @ServletComponentScan 을 작성해준다.
  • 기존 filter의 Component Annotation을 제거하고 @WebFilter(urlPatterns = “/api/user/*”) 처럼 url패턴으로 filter를 적용할 수 있다.
    • WebFilter는 배열로 pattern을 넣어줄 수 있다.

Interceptor

  • Interceptor란 filter와 매우 유사한 형태로 존재하지만 차이점은 Spring Context에 등록 된다는 점이다.
    • filter와 다르게 spring이 제공하는 기술로 디스패처 서블릿이 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조 및 가공할 수 있게해준다.
  • AOP와 유사한 기능을 제공 할 수 있으며, 주로 인증 단계를 처리하거나 Logging을 하는 데에 사용한다.
  • 이를 선/후 처리 함으로써, Service business logic과 분리 시킨다.

Interceptor 추가

  • Interceptor를 추가하기 위해서는 org.springframework.web.servlet의 HandlerInterceptor interface를 구현해야한다. 이는 다음의 3가지 메소드를 가지고 있다.
  • Interceptor interface에는 3개의 Method가 있다.
    1. preHandle 메소드
    • Controller가 호출되기 전에 실행된다.
    • true를 반환해야 다음 단계(다음 인터셉터 or 컨트롤러)로 진행이 된다.
      1. postHandle 메소드
    • Controller 호출 후에 실행된다.
      1. afterCompletion 메소드
    • 모든 작업이 완료된 후에 실행된다.

Interceptor 실습

  • Interceptor 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Slf4j
@Component
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String url = request.getRequestURI();

        URI uri = UriComponentsBuilder.fromUriString(request.getRequestURI()).query(request.getQueryString()).build().toUri();

        log.info("request url : {}", url);
        boolean hasAnnotation = checkAnnotation(handler, Auth.class);
        log.info("has annotation : {}", hasAnnotation);
        //서버는 모두 Public으로만 동작을 하고 Auth 권한을 가진 요청에 대해서는 세션, 쿠키 체크
        if(hasAnnotation){
            //권한 체크
            String query = uri.getQuery();
            log.info("query : {}", query);
            if(query.equals("name=kms")){
                return true;
            }
            throw new AuthException();
        }

        return true; // true가 되어야 interceptor를 넘어 내부 logic을 실행할 수 있다. False일 경우 return 된다.
    }

    private boolean checkAnnotation(Object handler, Class clazz){
        // resource, javascript, html
        if(handler instanceof ResourceHttpRequestHandler){
            return false;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        if(handlerMethod.getMethodAnnotation(clazz) != null || handlerMethod.getBeanType().getAnnotation(clazz) != null){
            return true;
        }

        return false;
    }
}
  • interceptor 추가
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    @Configuration
    @RequiredArgsConstructor    //final로 생성된 객체를 생성자로 주입받을 수 있게 해준다.
    public class MvcConfig implements WebMvcConfigurer {
    
      private final AuthInterceptor authInterceptor;
    
      @Override
      public void addInterceptors(InterceptorRegistry registry) {
          registry.addInterceptor(authInterceptor);
    //        registry.addInterceptor(authInterceptor).addPathPatterns("/api/private/*"); -> 해당 path에서만 interceptor실행
      }
    }
    
  • test용 Controller추가
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    @RestController
    @RequestMapping("/api/private")
    @Auth
    public class PrivateController {
    
      @GetMapping("/hello")
      public String hello(){
          return "private hello";
      }
    }
    
1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/api/public")
public class PublicController {

    @GetMapping("/hello")
    public String hello(){
        return "public hello";
    }
}
  • Auth Annotation추가
    1
    2
    3
    4
    5
    
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    public @interface Auth {
    }
    
  • Auth Exception
1
2
public class AuthException extends RuntimeException{
}
  • Auth Exception handler
1
2
3
4
5
6
7
8
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(AuthException.class)
    public ResponseEntity authException(){
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("");
    }
}
  • AuthException에 대해서 401 UNAUTHORIZED Response 리턴
  • preHandle가 true를 리턴해야 내부 logic을 수행할 수 있다.
  • preHandle의 Object handler는 ControllerMapping, Model, Data Binding, Type, Formatting 등 정보를 가지고 있다.
  • checkAnnotation의 경우 handlerMethod를 사용해 Auth.class가 Annotation으로 들어있는지 확인해 True, False를 반환한다.
    • 해당 메서드에서 True가 반환되면 Auth Annotation이 설정 되어 있는 것이고 이는 권한 체크가 필요함을 의미한다.

Filter와 Interceptor차이

관리되는 컨테이너

  • Filter : Web Container
  • Interceptor : Spring Container

용도

  • Filter
    • Spring과 무관하게 전역에서 공통으로 필요한 작업
    • 데이터 압축 및 인코딩
    • 보안관련 공통 작업(XXS 방)
    • 모든 요청에 대한 로깅
  • Interceptor
    • 인증,인가 관련 공통작업
    • Controller로 넘어가는 혹은 Controller에서 넘어오는 데이터의 가공
    • API호출에 대한 로깅
This post is licensed under CC BY 4.0 by the author.