[Spring] spring-boot Filter & Interceptor

99winnmin·2022년 7월 12일
0

Spring

목록 보기
5/17

Filter란?

Web Application에서 관리되는 영역(Handler object같은 것이 없음)으로써 Spring Boot Framework에서 Client로부터 오는 request/response에 대해서 최초/최종 단계의 위치에 존재하며, 이를 통해서 request/response의 정보를 변경하거나, Spring에 의해서 데이터가 변환되기 전의 순수한 Client의 요청/응답 값을 확인할 수 있다.

  • 유일하게 ServletRequest, ServletResponse의 객체를 변환할 수 있다.
  • 주로 Spring Framework에서는 request/response의 logging 용도로 활용하거나, 인증과 관련된 logic들을 해당 filter에서 처리한다.
  • 이를 선/후 처리함으로써, Service business logic과 분리시킨다.
  • Filter test 코드
@Slf4j // logging을 위한 annotation
@RestController
@RequestMapping("/api/filter")
public class FilterApiController {

    @PostMapping("/post")
    public User user(@RequestBody User user){
        log.info("User : {}",user); // {}안에 객체의 toString()이 매칭이됨
        return user;
    }
}
  • Filter 동작 코드
//@Component // spring에 등록하는 방식, 모든 controller에 동작하게됨
@Slf4j
@WebFilter(urlPatterns = "/api/filter/post/*") // 특정 class에만 filter를 적용시키고 싶을 때, urls : 여러개를 설정할 수 도 있음
public class GlobalFilter implements Filter { // javax.servlet.* 상속
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 전처리
        // ContentCaching : 내용을 미리 담고 커서가 이동하면서 읽음 -> 실질적인 데이터의 커서가 이동x
        // 하지만 맨처음에는 길이기반으로 초기화되고 ContentCachingRequestWrapper 내부에서 read,write하는 부분은 따로 있음음
        ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request);
        ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);

        // doFilter를 통해 내부로 들어가 메서드가 실행되고 그 결과 content를 읽을 수 있음
        // 따라서 doFilter 이후에 log를 찍어봐야함!!!!
        chain.doFilter(httpServletRequest, httpServletResponse);

        String url = httpServletRequest.getRequestURI();

        // 후처리
        // 위에서 읽은 내용을 logging 할 수 있음
       String reqContent = new String(httpServletRequest.getContentAsByteArray());
        log.info("request : {}, requestBody : {}",url, reqContent);

        String resContent = new String(httpServletResponse.getContentAsByteArray());
        int httpStatus = httpServletResponse.getStatus();

        // getContentAsByteArray()로 다 읽어버려서 body의 커서가 끝까지 내려감...
        // 따라서 읽은만큼 복사해줘야 client가 제대로된 response를 받을 수 있음
        httpServletResponse.copyBodyToResponse();

        log.info("response status : {}, responseBody : {}",httpStatus, resContent);

    }
}

Interceptor란?

Filter와 매우 유사한 형태로 존재하지만, 차이점은 Spring Context에 등록되기 때문에 annotation, class들을 활용할 수 있다.
AOP와 유사한 기능을 제공할 수 있으며, 주로 인증 단계를 처리하거나, Logging을 하는데에 사용한다.
이를 선/후 처리함으로써, Service business logic과 분리시킨다.

Interceptor활용 예제 코드

  • custom auth annotation
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target( {ElementType.TYPE, ElementType.METHOD})
public @interface Auth {
}
  • 권한이 요구되는 객체 vs 아닌 객체
@RestController
@RequestMapping("/api/private")
@Auth
@Slf4j
public class PrivateController { // 내부 사용자 혹은 세션이 인증된 사용자만 사용

    // @Auth 특정 메서드에 걸어줄 수 도 있지만 이런 방식은 유지보수가 어렵기 때문에
    // controller에 걸어주거나 특정 url에 매칭시켜주는것이 좋음
    @GetMapping("/hello")
    public String hello(){
        log.info("private hello controller");
        return "private hello";
    }
}

vs

@RestController
@RequestMapping("/api/public")
public class PublicController { // Open API, 아무나 사용가능

    @GetMapping("/hello")
    public String hello(){
        return "public hello";
    }
}
  • interceptor처리 코드
@Slf4j
@Component
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 원래대로라면 filter에서와 했던 것과 마찬가지로 ContentCachingRequestWrapper로 형변환하고
        // doFilter메서드를 실행해야함(request의 커서가 다 이동되서 읽을 내용이 없어지는 이슈를 해결하기 위함)
        // filter에서 전처리 -> interceptor에서 값을 받아서 형변환해야함
        // 그렇지 않으면 HttpServletRequest를 interceptor에서 형변환해줄 수 없음
        // 하지만 이 코드에서는 일단은 filter의 전처리 없이 그냥 진행
        String url = request.getRequestURI();

        URI uri = UriComponentsBuilder.fromUriString(request.getRequestURI())
                .query(request.getQueryString())
                .build().toUri(); // uri 에서 query 파싱

        log.info("request : {}", url);
        boolean hasAnnotation = checkAnnotation(handler, Auth.class);
        log.info("has annotation : {}", hasAnnotation);

        // 나의 서버는 모두 public으로 동작을 하는데
        // 단! Auth 권한을 가진 요청에 대해서는 세션, 쿠키등을 검사함
        if(hasAnnotation){ // 우리가 원하는 annotation으로 처리하는 방식
            // 권한체크
            String query = uri.getQuery(); // 쿼리가 같으면 통과? 보통은 쿠키나 세션을 검사함
            log.info("query : {}", query);
            if(query.equals("name=steve")){
                return true;
            }
            throw new AuthException(); // 권한이 없음을 예외처리
            // return false;
        }

        return true; // true가 되어야 interceptor를 통과할 수 있음음
    }
    private boolean checkAnnotation(Object handler, Class clazz){
        // resource(js, html)은 무조건 통과
        if(handler instanceof ResourceHttpRequestHandler){
            return true;
        }

        // annotation check
        HandlerMethod handlerMethod = (HandlerMethod) handler;
            // annotation이 달려있는지 check
        if(null != handlerMethod.getMethodAnnotation(clazz) || null != handlerMethod.getBeanType().getAnnotation(clazz)){
            // Auth annotation이 있을 때는 true
            return true;
        }

        return false;
    }
}
  • interceptor를 spring component로 등록하는 코드
@Configuration
@RequiredArgsConstructor // final로 선언된 객체들을 생성자로 주입받을 수 있도록 함
public class MvcConfig implements WebMvcConfigurer {
    // @Autowired 로도 받을 수 있지만 순환 참조가 일어날 수 도 있음
    private final AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // addPathPatterns으로 특정 url에 대해서만 AuthInterceptor를 동작하게 할 수 있음, 필요한 주소들을 여러개 추가할 수도 있음
        // registry.addInterceptor(authInterceptor).excludePathPatterns("/api/private/*"); 다음과 같이 제외할 수도 있음
        // 여러가지 interceptor가 동작할 수 있는데 그것은 코딩된 순서대로 동작함 -> 인증과정 depth 구현가능!
        registry.addInterceptor(authInterceptor).addPathPatterns("/api/private/*");
    }
}

출처 : 한 번에 끝내는 Java/Spring 웹 개발 마스터 초격차 패키지 Online.

profile
功在不舍

0개의 댓글