Filter

원승현·2024년 6월 2일

spring

목록 보기
9/21
post-thumbnail

공통 관심 사항

로그인을 해야 실행할 수 있는 기능이 존재
각 기능별로 로그인 유무 검증을 할 수 있지만 이것은 매우 귀찮고 유지보수 측면에서 좋지 않다.

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

서블릿 필터

필터 흐름
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 respons
    e, FilterChain chain) thorws IOException, ServletException;
    
    public default void destory() {}
    
}

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

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

Filter

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");
    }
}

chain.doFiler(request, response)

  • 이 부분이 가장 중요하다. 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다. 만약 이 로직을 호출하지 않으면 다음 단계로 진행하지 않는다.

Config

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을 사용해서 등록하면 된다.

로그인 검증 예시

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);
requestURI);
                }
//로그인으로 redirect httpResponse.sendRedirect("/login?redirectURL=" +
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);
} }

Controller

/**
* 로그인 이후 redirect 처리 */
 @PostMapping("/login")
 public String loginV4(
         @Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
         @RequestParam(defaultValue = "/") String redirectURL,
         HttpServletRequest request) {
     if (bindingResult.hasErrors()) {
         return "login/loginForm";
}
     Member loginMember = loginService.login(form.getLoginId(),
 form.getPassword());
     log.info("login? {}", loginMember);
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다."); return "login/loginForm";
}
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관 session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
//redirectURL 적용
     return "redirect:" + redirectURL;
 }
참고자료: 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술(인프런 김영한)

0개의 댓글