Spring/Spring MVC

스프링 MVC - 서블릿 필터

taey 2024. 10. 1. 23:40

서블릿 필터

필터 흐름

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러

필터를 적용하면 필터가 호출 된 다음에 서블릿이 호출된다. 필터는 특정 URL 패턴에 적용할 수 있다. /*이라고 하면 모든 요청에 필터가 적용된다. 스프링을 사용하는 경우 여기서 말하는 서블릿은 디스패처 서블릿이다.

 

필터 제한

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

 

필터 체인

HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러

 

필터는 체인으로 구성되는데, 중간에 필터를 추가할 수 있다. 예를 들어, 로그를 남기는 필터를 먼저 적용하고, 다음에 로그인 여부를 체크하는 필터를 만들 수 있다.

 

필터 인터페이스

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() {}
     
}

 

필터 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고, 관리한다.

  • init() : 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.
  • doFilter() : 고객의 요청이 올 때마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.
  • destroy() : 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.

서블릿 필터 - 요청 로그

LogFilter - 로그 필터

package hello.login.web.filter;

import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;

@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 {
     	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 destroy");
     }
}
  • public class LogFilter implements Filter {}
    • 필터를 사용하려면 필터 인터페이스를 구현해야 한다.
  • doFilter (ServletRequest request, ServletResponse response, FilterChain chain)
    • HTTP 요청이 오면 doFilter가 호출된다.
    • ServletRequest request는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스이다. HTTP를 사용하면 HttpServletRequest httpRequest = (HttpServletRequest) request;와 같이 다운 캐스팅하면 된다.
  • String uuid = UUID.randomUUID().toString();
    • HTTP 요청을 구분하기 위해 요청당 임의의 uuid를 생성해둔다.
  • log.info("REQUEST [{}]{{}]", uuid, requestURI);
    • uuid와 requestURI를 출력한다.
  • chain.doFilter(request, response);
    • 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다. 만약 이 로직을 호출하지 않으면 다음 단계로 진행되지 않는다.

 

WebConfig - 필터 설정

package hello.login;

import hello.login.web.filter.LogFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;

@Configuration
public class WebConfig {
     
     @Bean
     public FilterRegistrationBean logFilter() {
		FilterRegistrationBean<Filter> filterRegistrationBean = new
		FilterRegistrationBean<>();
		filterRegistrationBean.setFilter(new LogFilter());
		filterRegistrationBean.setOrder(1);
		filterRegistrationBean.addUrlPatterns("/*");
		
        return filterRegistrationBean;
     }
}

 

필터를 등록하는 방법은 여러가지가 있지만, 스프링 부트를 사용한다면 FilterRegistrationBean을 사용해서 등록하면 된다.

  • setFilter(new LogFilter()) : 등록할 필터를 지정한다.
  • setOrder(1) : 필터는 체인으로 동작한다. 따라서 순서가 필요하다. 낮을 수록 먼저 동작한다.
  • addUrlPatterns("/*") : 필터를 적용할 URL 패턴을 지정한다. 한 번에 여러 패턴을 지정할 수 있다.
참고
URL 패턴에 대한 규칙은 필터도 서블릿과 동일하다.

@ServletComponentScan @WebFilter(filterName = "logFilter", urlPatterns = "/*")로 필터 등록이 가능하지만 필터 순서 조절이 안 된다. 따라서 FilterRegistrationBean을 사용하는 것이 좋다.

HTTP 요청 시 같은 요청의 로그에 모두 같은 식별자를 자동으로 남기는 방법은 logback mdc로 확인

 


서블릿 필터 - 인증 체크

예시 코드

package hello.login.web.filter;

import hello.login.web.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.PatternMatchUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@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);
         
				//로그인으로 redirect
				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);
	}
}
  • whitelist = {"/", "/members/add", "/login", "/logout", "/css/*"};
    • 인증 필터를 적용해도 홈, 회원가입, 로그인 화면, css 같은 리소스에는 접근할 수 있어야 한다. 이렇게 화이트 리스트 경로는 인증과 무관하게 항상 허용한다. 화이트 리스트를 제외한 나머지 모든 경로에는 인증체크 로직을 적용한다.
  • isLoginCheckPath(requestURI)
    • 화이트 리스트를 제외한 모든 경우에 인증 체크 로직을 적용한다.
  • httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
    • 미인증 사용자는 로그인 화면으로 리다이렉트 한다. 그런데 로그인 이후에 다시 홈으로 이동해버리면, 원하는 경로를 다시 찾아가야 하는 불편함이 있다. 기존 보고 있던 페이지로 이동하기 위해, 현재 요청한 경로인 requestURI를 /login에 쿼리 파라미터로 함께 전달한다. 물론 /login 컨트롤러에서 로그인 성공시 해당 경로로 이동하는 기능은 추가로 개발해야 한다.
  • return;
    • 필터를 더는 진행하지 않는다. 이후 필터는 물론 서블릿, 컨트롤러가 더는 호출되지 않는다. 앞서 redirect를 사용했기 때문에 redirect가 응답으로 적용되고 요청이 끝난다.

 

WebConfig - loginCheckFilter() 

@Bean
public FilterRegistrationBean loginCheckFilter() {
	FilterRegistrationBean<Filter> filterRegistrationBean = new
	FilterRegistrationBean<>();
	filterRegistrationBean.setFilter(new LoginCheckFilter());
	filterRegistrationBean.setOrder(2);
	filterRegistrationBean.addUrlPatterns("/*");
	
    return filterRegistrationBean;
}
  • setFilter(new LoginCheckFilter()) : 로그인 필터를 등록한다.
  • setOrder(2) : 순서를 2번으로 잡았다. 로그 필터 다음에 로그인 필터가 적용된다.
  • addUrlPatterns("/*") : 모든 요청에 로그인 필터를 적용한다.

 

참고
필터에는 인터셉터에서 제공하지 않는 기능이 있다.
chain.doFilter(request, response); 를 호출해서 다음 필터 또는 서블릿을 호출할 때 request, response를 다른 객체로 바꿀 수 있다. ServletRequest, ServletResponse를 구현한 다른 객체를 만들어서 넘기면 해당 객체가 다음 필터 또는 서블릿에서 사용된다. 

'Spring > Spring MVC' 카테고리의 다른 글

스프링 MVC - 예외 처리  (1) 2024.10.02
스프링 MVC - 스프링 인터셉터  (2) 2024.10.02
스프링 MVC - Bean Validation  (2) 2024.10.01
스프링 MVC - Validator 분리  (2) 2024.10.01
스프링 MVC 오류 코드와 메시지 처리  (1) 2024.10.01