서블릿 필터
로그인이 된 사용자만 상품 관리 페이지에 들어갈 수 있어야 하는데 로그인을 하지 않은 사용자가 URL을 직접 호출하면 상품 관리 화면에 들어가서 기능들을 사용할 수 있다.
웹에 관련된 공통 관심사를 처리할 때에는 서블릿 필터나 스프링 인터셉터를 사용하는 것이 좋다. 이 글에서는 서블릿 필터에 대해 배우도록 하겠다.
필터 흐름
HTTP 요청 ---> WAS ---> 필터 ---> 서블릿 ---> 컨트롤러
필터를 적용하면 피터가 호출된 다음에 서블릿이 호출되며 특정 URL 패턴에 적용할 수 있다.
로그인 한 사용자는 필터, 서블릿을 건너서 컨트롤러까지 호출하는데 로그인을 하지 않은 사용자는 필터에서 요청을 끝내버릴 수 있다. 로그인 여부 등을 체크하기에 좋은 기능이다.
필터는 체인으로 구성되기 때문에 필터를 여러 개 지정할 수 있다.
LogFilter 클래스
@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 {
log.info("log filter doFilter");
HttpServletRequest httpRequest = (HttpServletRequest) request;
// Http만 나오도록 다운캐스팅
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");
}
}
LogFilter 클래스이다. 필터가 시작되고 끝났을 때의 로그를 출력해주는 코드이다.
LoginCheckFilter 클래스
@Slf4j
public class LoginCheckFilter implements Filter {
private static final String[] whitelist = {"/", "/members/add", "/login", "/logout","/css/*"};
// whitelist는 인증 체크 필요 x
@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);
}
}
다음으로 로그인체크 필터 클래스이다. init과 destroy 메서드를 오버라이딩 하지 않았는데, 그 이유는 default값을 서블릿에서 지원하기 때문에 없어도 되어서 삭제했다. whitelist라는 목록이 있는데, 우리가 상품 관리를 하는 페이지는 로그인을 해야지 접근가능하지만 로그인 페이지와 회원가입 홈 페이지는 세션이 없더라도 접근이 가능해야하기 때문에 화이트리스트를 따로 설정해두어서 세션이 없어도 접근 가능하게 만들었다.
try catch 문을 보면 session이 null이면 로그인 페이지로 리다이렉트 한다. httpResponse.sendRedirect("/login?redirectURL=" + requestURI);로 리다이렉트 하고 return;을 통해 더 이상 진행하지 않고 나가게 된다.
session이 올바르게 진입했으면 chain.doFilter로 다음 chian으로 가거나 서블릿으로 향하는데 여기서는 서블릿으로 향할 목적이라서 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); // 체인 1번
filterRegistrationBean.addUrlPatterns("/*"); // 모든 Url에 다 적용
return filterRegistrationBean;
}
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2); // 체인 2번
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
@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;
}
로그인 컨트롤러에도 변화가 있었는데, 아래 return을 보면 redirect 뒤에 redirectURL이 있다. 이 뜻은 만약 사용자가 상품 등록을 하다 로그인이 안되어있어서 로그인을 했는데 홈 화면 페이지로 이동하면 불편한 서비스이니까 로그인을 한 뒤에도 상품 등록 페이지로 연결할 수 있도록 하는 코드이다.

위에 url을 보면 redirectURL=/items로 남겨져 있다.
LoginController에서 @RequestParam 으로 redirectURL을 받고 있어서 로그인을 하게 되면

홈 화면이 아니라 바로 items 화면으로 넘어가게 된다.