4 분 소요

필터, 인터셉터

로그인하지 않은 사용자가 로그인이 필요한 페이지에 접근할 경우 막아줄 수 있어야 한다. 그런 기능을 맡는 로직을
작성했을 때 여러 페이지에 대해서 해당 로직을 적용해야 한다면 유지 보수가 어려워진다. 이렇게 여러 기능에서 공통
으로 적용될 수 있는 내용을 공통 관심사라고 하고 서블릿에서는 필터로, 스프링에서는 인터셉터로 해결할 수 있다.

서블릿 필터

다음 과정을 통해 렌더링이 이뤄진다. 필터는 서블릿 호출 직전에 거치게 된다!
만약 필터에서 적절하지 않은 요청이라 판단된다면 서블릿을 호출하지 않는다.
여러 필터를 적용할 수 있으며 순서 또한 설정할 수 있다.

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


필터 interface는 다음과 같다.

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(): 필터 종료 메소드로, 서블릿 컨테이너 종료 시 호출

사용자가 로그인했는지 검증하는 필터를 만들어보자. 이름은 LoginCheckFilter라 하자.
javax.servletFilter를 구현하면 되고, init(), destroy()는 default 메소드 여서 반드시 구현하지 않아도 된다.
doFilter()만 구현하면 다음과 같다.

public class LoginCheckFilter implements Filter {

    //화이트 리스트: 인증 체크X
    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 {
            //체크 대상 페이지 진입
            if(isLoginCheckPath(requestURI)) {
                HttpSession session = httpRequest.getSession(false);

                //미인증 사용자라면
                if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
                    //로그인 페이지로 redirect
                    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);
    }
}
  • isLoginCheckPath(): 권한이 필요한 페이지인지 체크
  • httpResponse.sendRedirect(): 강제로 이동할 페이지를 설정
    • ?redirectURL=를 설정함으로써 로그인 후 requestURI로 이동할 수 있도록 정보를 URL에 남김
    • 다음 필터 or 서블릿을 호출하지 않도록 리턴
  • chain.doFilter(): 다음 필터 or 서블릿 호출 (반드시 호출해야 함)

LoginCheckFilter는 다음과 같이 등록해서 사용할 수 있다!
WebMvcConfigurer를 구현한 WebConfig에서는 서블릿 필터, 스프링 인터셉터 모두 등록할 수 있다.

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

        return filterRegistrationBean;
    }
}
  • setFilter(): 구현한 필터 등록
  • setOrder(): 필터 순서 설정
  • addUrlPatterns(): 필터 적용 범위 설정 ("/*": 모든 요청)

위에서 미인증 사용자의 경우 권한이 필요한 페이지 접근 시 redirect 처리를 했지만 로그인 후 ?redirectURL=
에 있는 requestURI로 이동할 수 있도록 추가로 처리가 필요하다.

@PostMapping("/login")
public String loginV4(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
                      @RequestParam(defaultValue = "/") String redirectURL,
                      HttpServletRequest request) {
  ...
  return "redirect:" + redirectURL;
}
  • @RequestParam(defaultValue = "/"): 위에서 설정한 requestURI 값을 얻음(없으면 기본값 "/")
  • 그렇게 얻은 redirectURL로 redirect 처리 완료

스프링 인터셉터

서블릿 필터와 같은 기능을 스프링 MVC에서도 스프링 인터셉터라는 이름으로 제공한다.
모두 웹 관련 공통 관심 사항을 처리하지만 적용 순서, 기능이 서블릿 필터와 다르다.
일단 필터의 경우 서블릿 직전에 위치하지만 인터셉터는 컨트롤러 직전에서 호출된다.
인터셉터 또한 순서를 설정할 수 있다.

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러


다음은 스프링 인터셉터 interface이다.

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(): 컨트롤러 호출 전
  • postHandle(): 컨트롤러 호출 후. 에러 발생 시 호출X, modelAndView 정보 확인 가능
  • afterCompletion(): 뷰 렌더링 이후. 에러 발생 상관없이 반드시 호출, 에러 로그 확인 가능(ex)
  • 모든 함수가 default여서 필요한 함수만 구현하면 됨

스프링 인터셉터를 활용한 로그인 체크 로직은 다음과 같다. 이름은 LoginCheckInterceptor라 하자.
HandlerInterceptor를 구현하도록 설정해준 뒤 preHandle()에 로그인 검증 로직을 써보자.

public class LoginCheckInterceptor implements HandlerInterceptor {

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

        String requestURI = request.getRequestURI();

        HttpSession session = request.getSession();

        //미인증 사용자라면
        if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
            //로그인 페이지로 redirect
            response.sendRedirect("/login?redirectURL=" + requestURI);
            return false;
        }

        return true;
    }
}
  • 서블릿 필터와 전체적인 로직은 유사함
  • return
    • true : 정상 호출. 다음 인터셉터 or 컨트롤러 호출
    • false : 다음 인터셉터 or 컨트롤러 호출X

WebConfig에서 다음과 같이 스프링 인터셉터를 등록해줄 수 있다!
페이지 설정을 세세하게 할 수 있고, 여기서 설정을 해주기 때문에 서블릿 필터처럼 별도로 화이트리스트를 만들어
설정해줄 필요가 없다.

public class WebConfig implements WebMvcConfigurer {
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(new LogInterceptor())
              .order(1)
              .addPathPatterns("/**")
              .excludePathPatterns("/css/**", "/*.ico", "/error");

      registry.addInterceptor(new LoginCheckInterceptor())
              .order(2)
              .addPathPatterns("/**")
              .excludePathPatterns("/", "/members/add", "/login", "/logout",
                      "/css/**", "/*/ico", "/error");
  }
}
  • addInterceptor(): 구현한 스프링 인터셉터 등록
  • order(): 스프링 인터셉터 순서 설정
  • addPathPatterns(): 권한이 있어야 접근 가능한 페이지 설정
  • excludePathPatterns(): addPathPatterns()에 설정한 페이지 중 예외 처리

(추가) ArgumentResolver

이전에 사용자의 세션이 유지되고 있는지 체크하기 위해 다음 코드를 사용했었다.

public String homeLoginV3Spring(
        @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {
  ...
}


위 코드로도 정상적으로 동작하지만, 파라미터가 복잡하다는 단점이 있다.
ArgumentResolver를 통해 @Login 애노테이션을 만들어서 적용하면 훨씬 편리하게 처리할 수 있다!

public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {
  ...
}


일단 @Login 애노테이션 정의를 다음과 같이 해주자.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
  • @Target(ElementType.PARAMETER): 파라미터에만 사용
  • @Retention(RetentionPolicy.RUNTIME): 런타임까지 애노테이션 정보가 남아있음 (일반적으로 사용)

@Login 처리를 통해 loginMember가 적절한 정보를 얻을 수 있도록 설정해주는 코드이다.
HandlerMethodArgumentResolver를 구현하도록 설정해주자.

public class LoginMemberArugmentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        //@Login이 있는가?
        boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
        //Member 타입인가?
        boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());

        return hasLoginAnnotation && hasMemberType;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        HttpSession session = request.getSession(false);
        if(session == null) {
            return null;
        }

        //세션에서 로그인 회원 정보를 찾아 반환
        return session.getAttribute(SessionConst.LOGIN_MEMBER);
    }
}
  • supportsParameter(): @Login이 있고 Member 타입인지 체크
  • resolveArgument(): 세션에서 로그인 회원 정보 반환

WebConfig에서 생성한 ArgumentResolver LoginMemberArugmentResolver를 등록해주자.

public class WebConfig implements WebMvcConfigurer {

  @Override
  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
      resolvers.add(new LoginMemberArugmentResolver());
  }
}


서블릿 필터든 스프링 인터셉터든 공통 관심 사항을 처리할 수 있으나 스프링 인터셉터가 사용하는 코드가 적고
관리하기 편하다는 장점이 있어서 특별한 이유가 없는 이상 스프링 인터셉터를 많이 사용한다고 한다!

카테고리:

업데이트:

댓글남기기