서블릿 필터, 스프링 인터셉터

zwon·2023년 9월 5일
0

개발일지

목록 보기
11/23

세션을 가지고 로그인 기능을 구현했지만 아직도 여전히 문제가 많이 있었다.
로그인을 하지 않더라도 url경로만 알고있다면 접근이 가능했다.
그래서 이러한 문제를 없애고자 서블릿은 필터, 스프링은 인터셉터를 제공한다.

AOP를 사용할 수 있지만 웹과 관련된 공통 관심사 처리같은 경우는 HTTP 정보들이 필요할 수 있어 필터나 인터셉터를 사용하는 것이 더 좋다.

서블릿 필터

필터를 사용하면 다음과 같은 프로세스를 거친다.

  • 로그인에 성공한 사용자는 위와 같은 프로세스를 거치겠지만, 로그인에 성공하지 못한 사용자는 아래와 같은 프로세스를 거친다.

  • 필터에서 걸러졌기때문에 서블릿을 호출하지 못한다.

참고로 필터는 여러 개 추가가 가능하다. = 필터 체인

필터 인터페이스

public interface Filter {

    default void init(FilterConfig filterConfig) throws ServletException {}
    
    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException;
            
    default void destroy() {
    }
}
  • init()는 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출
  • doFilter()는 사용자 요청이 올 때마다 실행되는 메서드로 필터 로직은 doFilter()에 구현하면 된다.
    • doFilter()의 파라미터를 보면 ServletRequest인데 HttpServletXxxxx의 부모지만 사용할 수 있는 기능이 별로 없어 HttpServletXxxxx로 다운캐스팅을 하는 것을 추천한다.
  • destroy()는 필터 종료 메서드, 서블릿 컨테이너가 종료되면 호출

주의할 점이 doFilter()에 로직을 작성하고 doFilter()를 호출해줘야한다.
호출해줘야 다음 필터가 있으면 필터 호출, 없으면 서블릿을 호출해주기 때문이다.
만약 호출해주지 않으면 다음 단계로 넘어가지 않고 코드가 끝난다.

그 다음 필터를 빈으로 등록해줘야 사용할 수 있는데
1. @Component

  • 기본 URL Pattern이 /* 이며 설정할 수 없다.
  1. @Bean
  • 스프링 부트에서 필터 등록은 FilterRegistrationBean을 사용하여 등록
  • addUrlPatterns()을 사용해서 특정 URL에만 필터를 적용할 수 있다.

참고로 init()과 destroy()는 default여서 구현을 안해도 상관없다.

LoginCheckFilter 구현

@Slf4j
public class LoginCheckFilter implements Filter {
  private static final String[] whiteList = {"/", "/add/user", "/login","/logout", "/css/*"};

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

  @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 {
      if (!isLoginCheckPath(requestURI)) {
        // 화이트리스트에 속하지 않으면 로그인 인증 필터 적용
        HttpSession session = httpRequest.getSession(false);

        if (session == null || session.getAttribute("loginUser") == null) {
          // 로그인 성공 시 다시 본 페이지로 돌아올 수 있도록 redirectURL을 추가함.
          httpResponse.sendRedirect("/login?redirectURL"+requestURI);
          return;
        }
      }
      chain.doFilter(request, response);

    }catch(Exception e) {
      throw e;
    }
  }

  private boolean isLoginCheckPath(String requestURI) {
    return PatternMatchUtils.simpleMatch(whiteList, requestURI);
  }

  @Override
  public void destroy() {
    log.info("loginCheckFilter end");
  }
}
  • 화이트리스트에는 로그인 필터를 적용시키지 않을 URL을 추가했다.
  • isLoginCheckPath(String requestURI)는 스프링이 제공해주는 PatternMatchUtils를 사용해서 화이트리스트에 들어있는 URI들과 requestURI이 일치하면 true, 일치하지 않으면 false를 반환하게 하여 로그인 필터를 적용시킬 URI인지 아닌지를 판단하는 메서드이다.

doFilter()

  • 핵심 로직인 doFilter()를 따로 보자.
  @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 {
      if (!isLoginCheckPath(requestURI)) {
        // 화이트리스트에 속하지 않으면 로그인 인증 필터 적용
        HttpSession session = httpRequest.getSession(false);

        if (session == null || session.getAttribute("loginUser") == null) {
          // 로그인 성공 시 다시 본 페이지로 돌아올 수 있도록 redirectURL을 추가함.
          httpResponse.sendRedirect("/login?redirectURL="+requestURI);
          return;
        }
      }
      chain.doFilter(request, response);

    }catch(Exception e) {
      throw e;
    }
  }
  • 아까 위에서도 말했듯이 request,response들을 다운캐스팅 해주었다.
  • httpRequest.getRequestURI()를 통해 URI를 가져와서 isLoginCheckPath 메서드를 통해 필터를 적용시킬 URI인지 아닌지를 판단했다.
  • 만약 로그인 필터를 적용해야하는 URI면 세션을 가져오고 그 세션이 null이거나 loginUser가 null일 경우 다시 로그인 페이지로 redirect했다.
  • 그리고 로그인 성공 시 다시 원래 페이지로 돌아올 수 있도록 redirectURL을 추가해줌으로써 사용자에게 편리함을 제공하였다.
    • 회원가입 -> /library 접근 -> 로그인을 하지 않았기때문에 로그인 페이지로 이동 -> 로그인 성공 -> /library로 redirect
  • 그리고 로그인이 성공적이면 chain.doFilter(request, response);를 통해 다음 필터 또는 서블릿을 호출하도록 하였다.

Filter @Component 등록

  • 기본 URI가 /*로 설정되어 있다.
  • 하지만 우리는 화이트 리스트로 필터를 적용시키지 않을 URI들을 지정해두었기 때문에 상관없다.
@Component
public class LoginCheckFilter implements Filter {
 ...
}

Filter @Bean 등록

@Configuration
public class WebConfig {

  @Bean
  public FilterRegistrationBean loginCheckFilter() {
    FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean();
    filterRegistrationBean.setFilter(new LoginCheckFilter());
    filterRegistrationBean.setOrder(1); // 순서 지정
    filterRegistrationBean.addUrlPatterns("/*");
    return filterRegistrationBean;
  }
}
  • addUrlPatterns()을 통해 적용시킬 URI를 직접 지정할 수 있지만 우리는 화이트 리스트를 만들어두었기때문에 전체 경로에 적용되도록 함.
  • setFilter() : 필터 등록
  • setOrder() : 필터 체인이 가능하기떄문에 순서를 지정해줌
  • addUrlPatterns() : 필터를 적용시킬 URL

해당 프로젝트에선 @Component 등록 방식을 택했다.

마지막으로 로그인 성공 시 요청URL로 다시 redirect하게 해야하니 login 성공 시 컨트롤러를 수정야한다.

LoginController

  • @RequestParam String redirectURI 추가
public String login(@Validated @ModelAttribute Login login, BindingResult bindingResult,
                      @RequestParam(defaultValue = "/") String redirectURL,
                      HttpServletRequest request){
  ...
  ...
  // 로그인 성공 시
  return "redirect:"+redirectURI;
}
  • redirectURL의 디폴트값을 /로 설정해줌으로써 redirectURL이 없는 경우는 redirect:/가, redirectURL이 있는 경우는 redirect:"+redirectURI경로로 동작하게 함.

스프링 인터셉터

서블릿 필터가 아닌 스프링의 인터셉터를 적용시켜보자.
스프링 인터셉터는 서블릿 필터보다 정교한 URL 설정도 가능하고 더 편리하다.

  • 스프링 인터셉터는 스프링 MVC가 제공하는 기술이다.
  • 그래서 서블릿 이후에 스프링 인터셉터가 등장한다.
  • 인터셉터도 여러 개가 가능하다. = 인터셉터 체인
  • 인증 성공시 위와 같은 프로세스

  • 인증 실패시 위와 같은 프로세스

인터셉터 인터페이스

public interface HandlerInterceptor {

	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {}
            
	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable ModelAndView modelAndView) throws Exception {}

	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {}

}
  • preHandle() : 컨트롤러 호출 전
    • preHandle()이 true면 다음으로 진행하고 false면 진행하지 않는다.
    • preHandle() 실행 후 핸들러 어댑터, 핸들러(컨트롤러) 실행 후 ModelAndView를 반환 받으면 반환받은 ModelAndView를 postHandle()에 넘겨준다.
    • 근데 만약 컨트롤러에서 예외가 발생한다면?
  • postHandle() : 컨트롤러 호출 후
    • preHandle()이 ModelAndView를 반환한 다음 실행됨.
    • 컨트롤러에서 예외가 발생하면 호출X
  • afterCompletion() : HTTP 요청 완료 이후
    • view가 렌더링 된 후 실행됨.
    • 컨트롤러에서 예외가 발생하더라도 항상 호출
      • afterCompletion의 파라미터로 Exception이 있다.
      • 예외가 없다면 null이고 예외가 발생하면 그 예외가 들어있음.
    • 그래서 예외와 무관한 처리같은 경우는 afterCompletion()을 사용해야함.

그러면 생각해보면 로그인 인증은 컨트롤러 호출 전에 체크하면 된다.
그래서 preHandle()만 구현하면 끝이다.

인터셉텨가 편리한 이유
1. HTTPServletRequest를 다운캐스팅할 필요가 없다.
2. 인터셉터를 적용한 URL을 쉽게 등록할 수 있다.

구현을 해보자.

LoginCheckInterceptor

public class LoginInterceptor implements HandlerInterceptor {
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

    String requestURI = request.getRequestURI();
    HttpSession session = request.getSession(false);
    
    if (session == null || session.getAttribute("loginUser") == null){
      response.sendRedirect("/login?redirectURL="+requestURI);
      return false;
    }

    return true;
  }
}
  • 필터 코드랑 비슷한 흐름으로 가지만 차이점은 화이트리스트를 생성하고 그 requestURI가 화이트리스트에 포함되는지 안되는지의 로직이 없다.
  • 왜냐하면 인터셉터는 인터셉터를 등록할 때 다 설정할 수 있다.
  • 서블릿 필터보다 코드가 간결해진것을 볼 수 있다.

인터셉터 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LoginCheckInterceptor())
        .order(1)
        .addPathPatterns("/**") // 인터셉어틔 모든 경로 표현은 /**로 한다.
        .excludePathPatterns("/", "/add/user", "/login","/logout", "/css/*"); //화이트리스트
  }
}
  • 인터셉터는 implements WebMvcConfigurer를 구현하고 addInterceptors()를 통해 인터셉터를 등록할 수 있다.
  • excludePathPatterns()은 서블릿 필터의 화이트리스트같은 역할이라고 생각하면된다. excludePathPatterns()는 인터셉터가 적용되지 않는 부분이다.
profile
Backend 관련 지식을 정리하는 Back과사전

0개의 댓글