[TIL] #7. 로그인 처리2 - 필터, 인터셉터

kiteB·2021년 10월 2일
0

TIL-Spring4

목록 보기
13/17
post-thumbnail

서블릿 필터 - 소개

공통 관심 사항

📌 요구사항

  • 홈 화면 - 로그인 후
    • 본인 이름 (OO님 환영합니다.)
    • 상품 관리
    • 로그아웃

요구사항을 다시 살펴보면, 로그인한 사용자만 상품 관리 페이지에 접속할 수 있었다.

하지만 localhost:8080/items라는 URL만 알면,
로그인 유무와 관계없이 상품 관리 페이지에 접속되는 문제가 발생한다!😣

상품 관리 컨트롤러에서 로그인 여부를 체크하는 로직을 하나하나 작성하여 이 문제를 해결할 수도 있지만, 두 가지 문제점이 있다.

  • 등록/수정/삭제/조회 등등 모든 컨트롤러 로직에 공통으로 로그인 여부를 확인해야 한다.
  • 로그인과 관련된 로직이 변경될 때마다 일일이 수정해줘야 한다.

💡 공통 관심사 (cross-cutting concern)

이렇게 애플리케이션 여러 로직에서 공통으로 관심을 갖는 것공통 관심사라고 한다!

공통 관심사는 스프링의 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 response, FilterChain chain) throws IOException, ServletException;

    public default void destroy() {}
}

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

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

📌 init, destroy는 default 메서드이기 때문에 따로 구현하지 않아도 된다!


서블릿 필터 - 요청 로그

가장 단순한 필터인, 모든 요청을 로그로 남기는 필터를 개발해보자!

LogFilter

public class LogFilter implements Filter {}
  • 필터를 사용하려면 Filter 인터페이스를 구현해야 한다.
doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  • HTTP 요청이 오면 doFilter가 호출된다.
  • ServletRequest request는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스!
    • HTTP를 사용하려면 다운 캐스팅해주기!
String uuid = UUID.randomUUID().toString();
  • HTTP 요청을 구분하기 위해 요청 당 임의의 uuid를 생성해주자.
chain.doFilter(request, response);
  • 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다!
    이 로직을 호출하지 않으면 다음 단계로 진행되지 않는다!

WebConfig - 필터 설정

@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 패턴을 지정. 하나 이상의 패턴 지정 가능!
    • 여기서는 /*로 지정했기 때문에 모든 요청에 적용됨

🔗 전체 코드 확인하기


서블릿 필터 - 인증 체크

이번에는 인증 체크 필터를 개발하여,
로그인하지 않은 사용자가 상품 관리 뿐만 아니라 미래에 개발될 페이지에도 접근하지 못하도록 하자!

LoginCheckFilter - 인증 체크 필터

whitelist = {"/", "/members/add", "/login", "/logout","/css/*"};
  • 홈, 회원가입, 로그인 화면, 정적 리소스 (css 등) 같은 리소스는 로그인을 하지 않아도 접근할 수 있도록 해야 한다!
  • 이렇게 인증과 무관하게 항상 허용해야 하는 경우는 화이트 리스트 경로를 사용해서 접근을 허용해주자!
isLoginCheckPath(requestURI)
  • 매개변수로 전달받은 requestURI가 화이트리스트와 일치하는지 검사한다!
  • 화이트 리스트를 제외한 모든 경우에 인증 체크 로직을 적용한다.
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
  • 미인증 사용자는 로그인 화면으로 리다이렉트한다.
return;
  • 필터를 더는 진행하지 않는다.
  • 이후 필터는 물론 서블릿, 컨트롤러가 더는 호출되지 않는다.

WebConfig - loginCheckFilter() 추가

@Bean
public FilterRegistrationBean loginCheckFilter() {
    FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
    filterRegistrationBean.setFilter(new LoginCheckFilter());
    filterRegistrationBean.setOrder(2);
    filterRegistrationBean.addUrlPatterns("/*");
    return filterRegistrationBean;
}
  • setOrder(2): 순서를 2로 지정했기 때문에 로그 필터 다음에 로그인 필터가 적용된다.

🔗 전체 코드 확인하기


스프링 인터셉터 - 소개

이번에는 스프링 인터셉터에 대해 알아보자!

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

  • 서블릿 필터: 서블릿이 제공하는 기술
  • 스프링 인터셉터: 스프링 MVC가 제공하는 기술

둘다 웹과 관련된 공통 관심 사항을 처리하지만, 적용되는 순서와 범위, 그리고 사용방법이 다르다

🔗 차이점 자세히 알아보기


스프링 인터셉터

✔ 스프링 인터셉터 흐름

HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러

  • 스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출된다.
    • 스프링 인터셉터는 스프링 MVC가 제공하는 기능이기 때문에 디스패처 서블릿 이후에 등장하는 것.
    • 스프링 MVC의 시작점이 디스패처 서블릿!이라고 생각하면 될 듯😉
  • 스프링 인터셉터는 서블릿 URL 패턴과는 다르게, 매우 정밀하게 URL 패턴을 설정할 수 있다!

✔ 스프링 인터셉터 체인

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

  • 스프링 인터셉터도 체인으로 구성되어, 중간에 인터셉터를 자유롭게 추가할 수 있다.

✔ 스프링 인터셉터 인터페이스

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

단순하게 doFilter() 하나만 제공하는 서블릿 필터와는 달리, 인터셉터는 3단계로 세분화 되어 있다.

  • 컨트롤러 호출 전 (preHandle)
  • 호출 후 (postHandle)
  • 요청 완료 이후(afterCompletion)

또한, 서블릿 필터는 단순히 request, response만 제공했지만, 인터셉터는

  • 어떤 컨트롤러(handler)가 호출되는지
  • 어떤 modelAndView가 반환되는지

도 받을 수 있다.


✔ 스프링 인터셉터 호출 흐름

preHandle의 응답값이 true이면 다음으로 진행하고, false이면 더는 진행하지 않는다.
(false인 경우 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않는다. (1번에서 끝!))


✔ 스프링 인터셉터 예외 상황

예외가 발생시

  • preHandle: 컨트롤러 호출 전에 호출된다.
  • postHandle: 컨트롤러에서 예외가 발생하면 호출되지 않는다.
  • afterCompletion: afterCompletion은 항상 호출된다.
    • 그러므로 예외와 무관하게 공통 처리를 하려면 afterCompletion() 사용하기!

📌 정리

인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공한다고 이해하면 된다.
스프링 MVC를 사용하고, 특별히 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하자😃


스프링 인터셉터 - 요청 로그

LogInterceptor

request.setAttribute(LOG_ID, uuid)
  • 서블릿 필터는 지역변수를 사용하면 되지만, 스프링 인터셉터는 호출 시점이 완전히 분리되어 있다.preHandler에서 지정한 값을 postHandlerafterCompletion에서 사용하려면 어딘가에 담아둬야 한다!
  • LogInterceptor싱글톤처럼 사용되기 때문에 멤버 변수를 사용하면 안된다!
    request 인스턴스에 담아두자!
if (handler instanceof HandlerMethod) {
    HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
}
  • HandlerMethod
    • 스프링은 일반적으로 @Controller, @RequestMapping을 활용한 핸들러 매핑을 사용하는데, 이 경우에는 핸들러 정보로 HandlerMethod가 넘어온다.

WebConfig - 인터셉터 등록

public class WebConfig implements WebMvcConfigurer {

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

WebMvcConfigurer가 제공하는 addInterceptors()를 사용해서 인터셉터를 등록하기!

  • registry.addInterceptor(new LogInterceptor()): 인터셉터 등록
  • order(1): 인터셉터 호출 순서 지정 서블릿. 필터와 마찬가지로 순서가 낮을수록 먼저 동작!
  • addPathPatterns("/**") : 인터셉터를 적용할 URL 패턴을 지정
  • excludePathPatterns("/css/**", "/*.ico", "/error") : 인터셉터에서 제외할 패턴 지정

🔗 전체 코드 확인하기

profile
🚧 https://coji.tistory.com/ 🏠

0개의 댓글