김영한님의 mvc2 교재를 복습하며 정리합니다.
일단 수업 내용에 의존하지 않고 제 스스로 생긴 의문들을 바탕으로
정리하려고 노력하였습니다.
마찬가지로 수업을 배울 때 항상 실무자들은 어떻게 구조를 만드는지
눈여겨 보려고 노력합니다. 오늘 배운 부분도 구조를 분석합니다.
이번 수업에서는 Filter와 interceptor에 대해서 배웠고 둘은 web 폴더 아래 생성되어져 있습니다.
또한 ArgumentResolver 활용에 대해서 배웠습니다.
로그인한 사람만 들어갈 수 있는 페이지의 url
이를 로그인하지 않은 사람이 접속해서는 안된다.
따라서 로그인 여부를 체크하는 로직이 몇 십개의 페이지에 추가되어야 한다. 만약에 로그인 여부를 체크하는 로직을 수정해야할 때 몇 십개의 페이지를 들어가서 다 수정할 수 없는 노릇!
이와 같이 공통된 로직을 공통관심사라고 한다.
스프링의 AOP로도 해결할 수 있지만
웹과 관련된 공통 관심사는 서블릿 필터나 스프링의 인터셉터를 사용
웹과 관련된 공통 관심사를 처리할 때는 HTTP의 헤더나 URL의 정보들이 필요 -> 서블리 필터와 인터셉터는 HttpServletRequest 제공
필터는 서블릿이 지원하는 수문장
흐름
HTTP 요청 -> WAS -> 필터 -> 스프링의 디스패처 서블릿 -> 컨트롤러
필터에 특정 URL 패턴을 적용해서 어떤 URL은 통과, 미통과의 역할을 한다.
+) 디스패처 서블릿이란?
HTTP 프로토콜로 들어오는 모든 요청을 가장 먼저 받아 적합한 컨트롤러에 위임해주는 프론트 컨트롤러(Front Controller)
필터 제한
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 //로그인 사용자
HTTP 요청-> WAS -> 필터(적절하지 않은 요청 판단, 서블릿 호출 X)//비 로그인 사용자
필터 체인
HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러
공식 문서에 말하는 Filter 인터페이스를 보자
필터는 리소스에 대한 요청(서블릿 또는 정적 콘텐츠)이나 리소스의 응답 또는 둘 모두에 대해 필터링 작업을 수행하는 개체입니다.
필터 인터페이스를 구현해서 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리한다고 한다.
3가지의 메서드로 구성된다.
처음 만드는 서블릿 필터는 요청 로그를 찍는 서블릿 필터이다.
package hello.login.web.filter;
...
@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");
}
}
처음 서블릿 컨테이너가 생성될 때 "log filter init"
요청이 들어올 때마다 "REQUEST [{}][{}]", "RESPONSE [{}][{}]"
서블릿 컨테이너가 종료될 때 "log filter destroy"
FilterChain chain을 매개변수로 받는다.
FilterChain에 대한 공식문서의 설명은 아래와 같다.
FilterChain은 서블릿 컨테이너가 리소스에 대한 필터링된 요청의 호출 체인을 볼 수 있도록 개발자에게 제공하는 개체입니다. 필터는 필터 체인을 사용하여 체인의 다음 필터를 호출하거나 호출 필터가 체인의 마지막 필터인 경우 체인 끝에 있는 리소스를 호출합니다.
메서드도 단 하나이다.
...
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean =
new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
스프링 부트에서 필터를 등록할 때는 FilterResgistrationBean
을 사용해서 등록한다.
+) @ServletComponentScan과 @WebFilter(filterName="logFilter",urlPatterns="/*")로 필터 등록 가능하지만 필터 체인에 의한 순서 조절이 안된다.
요구사항
추후에도 추가되는 페이지에 대해서 로그인된 사용자만 접근할 수 있도록
따라서 로그인하지 않은 사용자가 접속 가능한 URL패턴만 배열로 저장하고
여기에 속하지 않으면 로그인 체크를 하는 필터를 추가하도록 해보자
@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);
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
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);
}
}
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
이 부분은 미인증 사용자는 다시 login페이지로 가는 것이다.
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean
= new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}