로그인 처리 - 필터, 인터셉터 (1)

2022. 4. 5. 20:44Spring 기초

로그인 처리 - 필터, 인터셉터 (1) 게시물은 필터를 다룹니다.

로그인 처리 - 필터, 인터셉터 (2)에서 인터셉터를 다루겠습니다.

 

필터

다음과 같은 상황이다.
내가 만든 웹사이트에 로그인 하지 않은 사용자는 회원가입, 홈 화면 외에는 접근을 막고싶다. 로그인 하지 않은 사용자가 url로 직접 호출하면 관리, 상세 페이지에 접근이 가능한 상황이다. 이를 막기위해서는

1. 모든 컨트롤러에서 로그인 여부를 체크하는 로직을 개발한다. 세션이 null이거나 session Attribute에서 id가 없으면 로그인 페이지로 쫓아내면 된다.
근데 이렇게 할 경우, 로그인 여부를 체크하는 로직이 변경된다면? 모든 컨트롤러를 다 뒤져가며 일일이 수정해야 한다. 심지어 몇몇 군데는 로그인 여부를 체크하는 로직이 누락될 수도 있다. 
이렇게 어플리케이션 여러 로직에서 공통으로 관심이 있는 부분을 공통관심사라고 한다.

이런 공통 관심사는 AOP로 처리 가능하지만, 지금의 로그인처럼 웹과 관련된 공통 관심사는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다. 왜냐하면 웹의 공통관심사를 처리할때는 HTTP 헤더, URL정보가 필요한데, 서블릿 필터와 스프링 인터셉터는 HttpServletRequest를 제공하므로 손쉽게 사용 가능하다.

각설하고 서블릿 필터에 대해 소개한다. 필터는 서블릿이 제공하는 기능이다(인터셉터는 스프링MVC가 제공)
필터의 위치는 WAS와 서블릿(스프링에서는 디스패처 서블릿이 front Servlet이다.) 사이에 위치한다.

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

따라서 모든 고객의 요청 로그를 남겨야 하는 경우, 로그인 여부를 살피는 경우 필터를 사용할 수 있다. 필터는 어떤 URL 패턴에 적용할 것인지 지정 가능하다. 

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

만약 로그인 하지 않은 사용자의 요청이라면 필터에서 걸러내는 방식이다. 필터는 체인 방식이라 필터를 여러개 추가할 수 있다. 전처리는 필터 1, 2, 3 순서로 수행되며 서블릿 실행 이후 후처리는 필터 3, 2, 1 역순으로 수행된다!!

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

이제 본격적으로 필터를 사용하는 방법을 알아보자. 

필터를 사용하기 위해서는 Filter 인터페이스를 구현해야 한다.

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(): 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.

init와 destory 메서드는 디폴트 메서드이다!

필터가 정말 수문장 역할을 잘 하는지 확인하기 위해 가장 단순한 필터인, 모든 요청을 로그로 남기는 필터를 개발하고 적용해보자!

 

@Slf4j
public class LogFilter implements Filter {  //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");
     }
}
doFilter(ServletRequest request, ServletResponse response, FilterChain chain)에서
HTTP 요청이 오면 doFilter 메서드가 호출된다.

ServletRequest request 는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스이다. HTTP를
사용하면 HttpServletRequest httpRequest = (HttpServletRequest) request; 와 같이
다운 케스팅 한 뒤 사용하면 된다.

 

try 문에서
chain.doFilter(request, response);
이 부분이 가장 중요하다. 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다. 
만약 이 로직을 호출하지 않으면 다음 단계로 진행되지 않는다!!!
컨트롤러 호출 없이 finally문 실행 후 종료되니 반드시 넣어주어야 한다.

 

이렇게 Filter 인터페이스를 구현하고 로직을 완성했다면, Filter를 등록하는 과정만이 남아있다.

@Configuration
public class WebConfig {

//스프링 부트를 사용한다면 지금처럼 FilterRegistrationBean 을
사용해서 등록하면 된다

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

필터는 체인으로 등록하므로, setOrder에서 필터 적용 순서를 정해주었으며, addUrlPatterns로 적용할 Url을 설정했다. 지금은 모든 url에 적용된다. 

만약 필터의 적용을 배제하고싶은 url이 있다면 어떻게 해야할까??

private static final String[] whitelist = {"/", "/members/add", "/login", "/logout","/css/*"};
/**
 * 화이트 리스트의 경우 인증 체크X
 */
 private boolean isLoginCheckPath(String requestURI) {
	 return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
 }

try {
     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; //미인증 사용자는 여기서 바로 종료해버린다.
 }
 }

filter를 사용하고 싶지 않은 url 패턴을 static String[] whiteList로 모은다음, url이 매칭되는지 체크하기 위해 PatternMatchUtils.simpleMath() 메서드를 활용한다. 이 메서드의 반환값인 boolean을 사용하여 필터의 로직을 구현하면 된다.