공통 관심사를 처리하기 위해 AOP로도 해결할 수 있지만, 웹과 관련된 공통 관심사는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다.
웹과 관련된 공통 관심사를 처리할 때는 HTTP의 헤더나 URL의 정보들이 필요한데, 서블릿 필터나 스프링 인터셉터는HttpServletRequest를 제공한다.
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() {
}
}
javax.servlet의 Filter 인터페이스를 사용해야한다.init() : 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출doFilter() : 고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.destroy() : 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.package hello.login.web.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("LogFilter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("LogFilter doFilter");
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("LogFilter destroy");
}
}
public class LogFilter implements Filter {}doFilter가 호출된다.ServletRequest는 HTTP요청이 아닌 경우까지 고려해서 만든 인터페이스이다.HttpServletRequest를 (HttpServletRequest)로 다운캐스팅 하며 된다.chain.doFilter(request, response)package hello.login.web.filter;
import hello.login.web.session.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.PatternMatchUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@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);
}
}
// 화이트 리스트의 경우 인증 체크X
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whiteList, requestURI);
}
}
whiteList = {"/", "/members/add", "/login", "/logout","/css/*"};httpResponse.sendRedirect("/login?redirectURL=" + requestURI);package hello.login.web;
import hello.login.web.filter.LogFilter;
import hello.login.web.filter.LoginCheckFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean<Filter> logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
@Bean
public FilterRegistrationBean<Filter> loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
setFilter(new LogFilter(new Filter)) : 등록할 필터를 지정한다.setOreder(1) : 필터는 체인으로 동작하기때문에 순서가 필요하다. 낮을수록 먼저 동작한다.addUrlPatterns("/*") : 필터를 적용할 URL패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다."/*"는 전체 경로를 뜻한다.HTTP요청시 같은 요청의 로그에 모두 같은 식별자를 자동으로 남기는 방법은 logback mdc를 검색.
로그인 성공시 처음 요청한 URL로 이동하는 기능
@PostMapping("/login")
public String loginV4(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if(loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
// 로그인 성공 처리
HttpSession session = request.getSession();
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:" + redirectURL;
}