[Spring] Filter/Interceptor (2)

Noah·2022년 1월 18일
0

Spring

목록 보기
8/8
post-thumbnail

Filter 예제

클라이언트로부터 받은 Request url, body와 서버의 Response status-code, body를 로깅 하는 간단한 예제이다.

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.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;

@Slf4j
@WebFilter(urlPatterns = "/api/user/*")
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("request url: {}, request body: {}", url, reqContent);

        String resContent = new String(httpServletResponse.getContentAsByteArray());
        int httpStatusCode = httpServletResponse.getStatus();
        log.info("response status : {}, responseBody: {}", httpStatusCode, resContent);

        httpServletResponse.copyBodyToResponse();
    }
}

Filter 인터페이스를 구현하면 되는데 구현할 수 있는 메서드는 크게 세 가지가 있다.

init()

init 메서드는 필터 객체를 초기화하고 서비스에 추가하기 위한 메서드이다. 웹 컨테이너가 1회 init 메서드를 호출하여 필터 객체를 초기화하면 이후의 요청들은 doFilter를 통해 처리된다.

doFilter()

doFilter 메서드는 HTTP 요청이 디스패처 서블릿으로 전달되기 전에 웹 컨테이너에 의해 실행되는 메서드이다. doFilter의 파라미터로는 FilterChain이 있는데, FilterChain의 doFilter 통해 다음 대상으로 요청을 전달하게 된다. chain.doFilter() 전/후에 우리가 필요한 처리 과정을 넣어줌으로써 원하는 처리를 진행할 수 있다.

destroy()

destroy 메서드는 필터 객체를 서비스에서 제거하고 사용하는 자원을 반환하기 위한 메서드이다. 이는 웹 컨테이너에 의해 1번 호출되며 이후에는 이제 doFilter에 의해 처리되지 않는다.


Interceptor 예제

모든 클라이언트가 접근할 수 있는 public api가 있고 특정 조건을 만족하는 클라이언트만 접근할 수 있는 private api가 있다. private api의 경우 @Auth 어노테이션을 갖고 있고 클라이언트로부터 적절한 쿼리를 받은 경우에만 접근을 허용한다.

package com.example.interceptor.interceptor;

import com.example.interceptor.annotation.Auth;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
import org.springframework.web.util.UriComponentsBuilder;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URI;

@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);

        if (hasAnnotation) {
            String query = uri.getQuery();
            if (query.equals("name=steve")) {
                // query로 name=steve를 보낸 클라이언트에만 응답할 private method
                return true;
            }
            // 그렇지 않으면 거절
            return false;
        }

        // 모든 클라이언트가 접근 가능한 public method
        return true;
    }

    private boolean checkAnnotation(Object handler, Class clazz) {
        if (handler instanceof ResourceHttpRequestHandler) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;

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

        return false;
    }
}

HandlerInterceptor 인터페이스를 구현하면 된다.

preHandle()

preHandle 메서드는 컨트롤러가 호출되기 전에 실행된다. 그렇기 때문에 컨트롤러 이전에 처리해야 하는 전처리 작업이나 요청 정보를 가공하거나 추가하는 경우에 사용할 수 있다.

preHandle의 3번째 파라미터인 handler 파라미터는 핸들러 매핑이 찾아준 컨트롤러 빈에 매핑되는 HandlerMethod라는 새로운 타입의 객체로써, @RequestMapping이 붙은 메서드의 정보를 추상화한 객체이다.

또한 preHandle의 반환 타입은 boolean인데 반환값이 true 이면 다음 단계로 진행이 되지만, false라면 작업을 중단하여 이후의 작업(다음 인터셉터 또는 컨트롤러)은 진행되지 않는다.

postHandle()

postHandle 메서드는 컨트롤러를 호출된 후에 실행된다. 그렇기 때문에 컨트롤러 이후에 처리해야 하는 후처리 작업이 있을 때 사용할 수 있다. 이 메서드에는 컨트롤러가 반환하는 ModelAndView 타입의 정보가 제공되는데, 최근에는 json 형태로 데이터를 주고받는 RestAPI 기반의 컨트롤러(@RestController)를 만들면서 자주 사용되지는 않는다.

afterCompletion()

afterCompletion 메서드는 이름 그대로 모든 뷰에서 최종 결과를 생성하는 일을 포함해 모든 작업이 완료된 후에 실행된다. 요청 처리 중에 사용한 리소스를 반환할 때 사용하기에 적합하다.

profile
개발 공부는 🌳 구조다…

0개의 댓글