로그인-필터, 인터셉터

업데이트:

서블릿 필터

공통 관심 사항

  • 애플리케이션 여러 로직에서 공통으로 관심이 있는 것을 공통 관심사(cross-cutting concern) 라고 한다.
  • 공통 관심사는 스프링의 AOP로도 해결할 수 있지만, 웹과 관련된 공통관심사는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다.
  • 웹과 관련된 공통 관심사를 처리할 때는 HTTP의 헤더나 URL의 정보들이 필요한데, 서블릿 필터나 스프링 인터셉터는 HttpServletRequest를 제공한다.

필터 흐름

  • HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
  • 필터를 적용하면 필터가 호출 된 다음에 서블릿이 호출된다.
  • 필터는 특정 URL 패턴에 적용할 수 있다.

필터 제한

  • HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 (로그인 사용자)
  • HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단하여 서블릿 호출X, 비 로그인 사용자)

필터 체인

  • HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러
  • 필터는 체인으로 구성되고 중간에 자유롭게 추가가 가능하다.
  • 데코레이터 패턴 비슷해 보임

필터 인터페이스

  • 필터 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤으로 생성하고 관리한다.
  • init() : 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출 된다.
  • doFilter() : 고객의 요청이 올떄마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.
  • destroy() : 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.
public interface Filter {
 public default void init(FilterConfig filterConfig) throws ServletException {}
 public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException;
 public default void destroy() {}
}

필터 사용 및 등록 방법

  • 필터를 사용하려면 필터를 구현해야 한다.
  • HTTP 요청이 오면 doFilter가 호출된다.
  • HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스다.
  • HTTP를 사용하려면 다운 케스팅 하면 된다.
  • chain.doFilter(request, response);
    • 가장 중요한 부분으로 다음 필터가 있으면 필터를 호출하고, 없으면 서블릿을 호출한다.
    • 이 로직을 호출하지 않으면 다음 로직으로 진행이 되지 않는다.
@Slf4j
public class LogFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("log filter init");

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("log filter doFilter");

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();
        String uuid = UUID.randomUUID().toString();

        try {
            log.info("REQUEST[{}][{}]", uuid, requestURI);
            chain.doFilter(request, response);
        } catch (Exception e) {
            throw e;
        } finally {
            log.info("RESPONSE [{}][{}]", uuid, requestURI);
        }
    }

    @Override
    public void destroy() {
        log.info("log filter doFilter");
    }
 }
  • 스프링 부트 사용시 FilterRegistrationBean을 등록해서 사용하면 된다.
@Configuration
public class WebConfig {
    
    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LogFilter());
        //실행 순서 지정 
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");

        return filterRegistrationBean;
    }

}

@ServletComponentScan, @WebFilter(filterName = “”, urlPatterns = “”) 로 등록이 가능하지만 필터 순서 조절이 되지 않는다.

실무에서 HTTP 요청시 같은 요청의 로그에 모두 같은 식별자를 자동으로 남기는 방법은 logback mdc로 검색

인증 체크

  • 인증 회원인지 미인증 회원인지 필터 추가를 통해 확인한다.
  • whiteList 를 통해 미인증 상황에서도 접근이 가능하도록 체크 해준다. 그렇지 않으면 무한으로 필터가 걸린다.
  • redirect를 사용해서 로그인 후 원래 화면으로 돌아가도록 설정한다.
  • 필터를 사용함으로써 로그인 정책이 변경 되어도 필터만 수정하면된다(SRP 준수)
  • 스프링 시큐리티도 필터를 통한 기능으로 구현되어 있다.
@Slf4j
public class LoginCheckFilter implements Filter {

    private static final String[] whiteList = {"/","/members/add","/login","/logout","/css/*"};

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();

        HttpServletResponse httpResponse = (HttpServletResponse) response;

        try {
            log.info("인증 체크 필터 시작{}", requestURI);


            if(isLoginCheckPath(requestURI)){
                log.info("인증 체크 로직 실행 {}", requestURI);
                HttpSession session = httpRequest.getSession(false);
                if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {

                    log.info("미인증 사용자 요청 {}", requestURI);
                    //로그인으로 reditrec
                    httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
                    return;
                }
            }
            chain.doFilter(request, response);
        } catch(Exception e) {
            throw e; //예외 로깅 가능하지만, 톰캣까지 에외를 보내주어야 함
        } finally {
            log.info("인증 체크 필터 종료 {}", requestURI);
        }
    }

    /**
     * 화이트 리스트의 경우 인증 체크 X
     * */
    private boolean isLoginCheckPath(String requestURI) {
        return PatternMatchUtils.simpleMatch(whiteList, requestURI);
    }

}

빈을 WebConfig에 등록한다


@Bean
public FilterRegistrationBean loginCheckFilter() {
    FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
    filterRegistrationBean.setFilter(new LoginCheckFilter());
    filterRegistrationBean.setOrder(2);
    filterRegistrationBean.addUrlPatterns("/*");

    return filterRegistrationBean;
}

댓글남기기