✔️ 필터란, J2EE 표준 스펙 기능으로 디스패처 서블릿(Dispatcher Servlet)에 요청이 전달되기 전/후에 URL 패턴에 맞는 모든 요청에 대해 부가작업을 처리할 수 있는 기능을 제공한다.
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 (적절한 요청)
HTTP 요청 -> WAS -> 필터(적절하지 않은 요청, 서블릿 호출하지 않음)
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() {}
}
@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 패턴을 지정한다. 한 번에 여러 패턴 지정도 가능하다.💡 참고 1 : URL 패턴은 필터와 서블릿이 동일하다. 서플릿 URL 패턴을 참고하자.
💡 참고 2 : 실무에서 HTTP 요청 시 해당 요청에 대한 로그에 모두 같은 식별자를 남기고 싶은 경우, logback mdc 를 사용하면 된다. 사용 방법은 검색해서 알아보자.
@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);
//로그인으로 redirect
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
return; // 미인증 사용자는 다음으로 진행하지 않고 종료
}
}
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("인증 체크 필터 종료 {}", requestURI);
}
}
/**
* 화이트 리스트의 경우 인증 체크 패스
* @param requestURI {@link String}
* @return boolean {@link Boolean}
*/
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}
}
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
: 미인증 사용자가 로그인 후 자신이 보고 있던 화면을 그대로 보여주기 위해 현재 요청 경로인 reuqestURI를 쿼리 파라미터로 함께 전달한다.💡 참고
✔️ 인터셉터란, Spring이 제공하는 기술로써 디스패처 서블릿(Dispatcher Servlet)이 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 기능을 제공한다.
✔️ 웹 컨테이너(서블릿 컨테이너)에서 동작하는 필터와 달리 인터셉터는 스프링 컨텍스트에서 동작한다.
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> Interceptor -> 컨트롤러
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> Interceptor -> 컨트롤러 (적절한 요청)
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> Interceptor(적절하지 않은 요청, 컨트롤러 호출하지 않음)
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(HttpServeltReqeust request
, HttpServletResponse response
, Object handler
, @Nullable Exception ex) throws Exception {}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
}
}
WebMvcConfigurer
가 제공하는 addInterceptors()
를 사용해서 인터셉터를 등록할 수 있다.
order(1)
: 호출할 인터셉터의 순서를 지정한다. 낮을 수록 먼저 호출된다.
addPathPatterns("/**")
: 인터셉터를 적용할 URL 패턴을 지정한다.
excludePathPatterns("/css/**", "/*.ico", "/error")
: 인터셉터에서 제외할 패턴을 지정한다.
💡 참고 : 스프링이 제공하는 URL 경로는 서블릿 URL 경로와 완전히 다르다. 스프링에서 제공하는 URL이 더욱 자세하고 세밀하게 설정할 수 있다.
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
public static final String LOG_ID = "logId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String uuid = UUID.randomUUID().toString();
request.setAttribute(LOG_ID, uuid);
//@RequestMapping : HandlerMethod
//정적 리소스 : ResourceHttpRequestHandler
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler; // 호출할 컨트롤러 메소드의 모든 정보가 포함되어 있다.
}
log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle [{}]", modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String requestURI = request.getRequestURI();
String logId = (String) request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}]", logId, requestURI);
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
String uuid = UUID.randomUUID().toString()
: 요청 로그를 구분하기 위한 uuidrequest.setAttribute(LOG_ID, uuid);
: 인터셉터는 각 메소드의 호출 시점이 분리되어 있기 때문에 preHandle()에서 사용했던 값을 다른 메소드에서 사용하기 위해 request.Settribute()를 통해 request에 담아서 사용할 수 있다.HandlerMethod
가 넘어온다.ResourceHttpRequestHandler
가 넘어온다.@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("인증 체크 인터셉터 실행 {}", requestURI);
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("미인증 사용자 요청");
//로그인으로 redirect
response.sendRedirect("/login?redirectURL=" + requestURI);
return false;
}
return true;
}
preHandle
: 컨트롤러 호출 전에 호출 됨postHandle
: 컨트롤러에서 예외가 발생하면 postHandler은 호출되지 않음afterCompletion
: afterCompletion은 항상 호출 됨. 예외 발생 시 ex를 포함해서 호출되며, 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력 가능Interceptor는 스프링 MVC 구조에 특화된 Filter 기능을 제공한다고 보면 됨. 특별히 Filter를 사용해야 하는 상황이 아니면, Interceptor를 사용하는 것이 편리함
✔️ 이 전에 포스팅했던 로그인 처리 - 쿠키 세션에서 로그인 회원 정보를 찾을 때 아래와 같이 사용했었다.
//HomeController
public String homeLoginV3Spring(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginmember,
Model model) {
if (loginmember == null) {
return "home";
}
model.addAttribute("member", loginmember);
return "loginHome";
}
✔️ 위 방법보다 좀 더 편리하게 회원 정보를 찾을 수 있도록 ArgumentResolver를 사용하여 작성해본다.
//HomeController
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {
//세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
//@Login Annotation
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
LoginMemberArgumentResolver
가 동작해서 자동으로 세션에 있는 로그인 회원을 찾아주고 없다면 null을 반환해주도록 ArgumentResolver를 만들어본다.✔️ 커스텀 ArgumentResolver
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer
, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
log.info("resolveArgument 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
supportsParameter()
: @Login
어노테이션이 있고 Member
타입이면 해당 LoginMemberArgumentResolver
가 실행된다.
resolveArgument()
: 컨트롤러 호출 직전에 호출되어서 필요한 파라미터 정보를 생성해준다. 위 예제에서는 세션에 있는 member 객체를 찾아서 반환해준다. 이후 스프링 MVC에서 컨트롤러 메소드를 호출하면서 해당 member 객체를 파라미터에 전달해준다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
//LoginMemberArgumentResolver 등록
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
...
}
참고 Reference