-
[Spring] 서블릿(Servlet)의 필터(Filter)기능 소개Back-end/Spring 2022. 4. 11. 11:26
안녕하세요 이번 포스팅은 서블릿에서 지원하는 필터 기능에 대해서 알아보겠습니다!!!
상황 가정
우선 다음과 같은 상황이있다고 가정해보겠습니다. 관리자는 한 쇼핑몰에 상품을 등록, 수정, 삭제를 하려고 하는데 이런 여러 로직을 수행하기 위해서는 관리자가 로그인이 되어있는지 확인을 해줘야 합니다.
물론 컨트롤러 단에서 session에 있는 정보를 꺼내서 모든 메서드마다 확인을 해줄 수 있지만 이것은 중복된 코드로 코드 가 비효율적이겠죠? 그렇기 때문에 저희는 서블릿 필터를 사용할 수 있습니다.
서블릿 필터 소개
필터는 서블릿이 지원하는 수문장이고 특성은 다음과 같습니다.
필터를 적용하면 필터가 호출된 다음에 서블릿이 호출됩니다. 그래서 모든 고객의 요청 로그를 남기는 요구사항이 있다면 필터를 사용하면 됩니다. 참고로 필터는 특정 URL 패턴에 적용할 수 있습니다. /*이라고 하면 모든 요청에 필터가 적용됩니다. 참고로 스프링을 사용하는 경우 여기서 말하는 서블릿은 스프링의 디스패처 서블릿(DispatcherServlet)으로 생각하면 됩니다.
필터에서 적절하지 않은 요청이라고 판단하면 거기서 끝을 낼 수도 있습니다. 그래서 로그인 여부를 확인하기 위해서 필터는 아주 유용한 기능이라 할 수 있습니다.
또한 필터는 체인으로 구성되는데 중간에 필터를 자유롭게 추가할 수 있습니다. 예를 들어서 로그를 남기는 필터를 먼저 적용하고 그다음에 로그인 여부를 체크하는 필터를 만들 수 있습니다.
필터 인터페이스
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() {} }
필터 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리합니다.
서블릿 필터 - 요청 로그
필터가 정말 수문장 역할을 잘하는지 확인하기 위해 가장 단순한 필터인, 모든 요청을 로그로 남기는 필터를 개발하고 적용해보겠습니다.
LogFilter - 로그 필터
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("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"); } }
1. 우선 필터를 사용하려면 필터 인터페이스를 구현해야 합니다.
- public class LogFilter implements Filter {}
2.doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- HTTP 요청이 오면 doFilter가 호출됩니다.
- ServletRequest request는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스입니다. HTTP를 사용하면
HttpServletRequest httpRequest = (HttpServletRequest) request;와 같이 다운 캐스팅하면 됩니다.
3.String uuid = UUID.randomUUID(). toString();
- HTTP 요청을 구분하기 위해 요청당 임의의 uuid를 생성해둡니다
4. log.info("REQUEST [{}][{}]", uuid, requestURI)
- uuid와 requestURI를 출력합니다.
5. chain.doFilter(request, response)
- 이 부분이 가장 중요합니다. 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출합니다. 만약 이 로직을 호출하지 않으면 다음 단계로 진행되지 않습니다.
WebConfig- 필터 설정
import hello.login.web.filter.LogFilter; 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 logFilter() { FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(); filterRegistrationBean.setFilter(new LogFilter()); filterRegistrationBean.setOrder(1); filterRegistrationBean.addUrlPatterns("/*"); return filterRegistrationBean; } }
필터를 등록하는 방법은 여러 가지가 있지만, 스프링 부트를 사용한다면 FilterRegistrationBean을 사용해서 등록하면 됩니다.
1.setFilter(new LogFilter()) 등록할 필터를 지정합니다.
2. setOrder(1) 필터는 체인으로 동작합니다 따라서 순서가 필요합니다. 낮을수록 먼저 동작합니다.
3. addUrlPatterns("/*") 필터를 적용할 URL 패턴을 지정합니다. 한 번에 여러 패턴을 지정할 수 있습니다.
실행 로그
hello.login.web.filter.LogFilter: REQUEST [0a2249f2- cc70-4db4-98d1-492ccf5629dd][/items] hello.login.web.filter.LogFilter: RESPONSE [0a2249f2- cc70-4db4-98d1-492ccf5629dd][/items]
필터를 등록할 때 urlPattern을 /* 로 등록했기 때문에 모든 요청에 해당 필터가 적용됩니다.
서블릿 필터 - 인증 체크
이제는 인증 체크 필터를 개발해보겠습니다. 로그인되지 않은 사용자는 상품 관리뿐만 아니라 미래에 개발될 페이지에도 접근하지 못하도록 해보겠습니다.
LoginCheckFilter - 인증 체크 필터
import hello.login.web.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); } }
1. whitelist = {"/", "/members/add", "/login", "/logout", "/css/*"};
- 인증 필터를 적용해도 홈, 회원가입, 로그인 화면, css 같은 리소스에는 접근할 수 있어야 합니다. 이렇게 화이트 리스트 경로는 인증과 무관하게 항상 허용합니다. 화이트 리스트를 제외한 나머지 모든 경로에는 인증 체크 로직을 적용합니다.
2. isLoginCheckPath(requestURI)
- 화이트 리스트를 제외한 모든 경우에 인증 체크 로직을 적용합니다.
3. httpResponse.sendRedirect("/login? redirectURL=" + requestURI);
- 미인증 사용자는 로그인 화면으로 리다이렉트 합니다. 그런데 로그인 이후에 다시 홈으로 이동해버리면, 원하는 경로를 다시 찾아가야 하는 불편함이 있습니다. 예를 들어서 상품 관리 화면을 보려고 들어갔다가 로그인 화면으로 이동하면, 로그인 이후에 다시 상품 관리 화면으로 들어가는 것이 좋습니다. 이런 부분이 개발자 입장에서는 좀 귀찮을 수 있어도 사용자 입장으로 보면 편리한 기능입니다. 이러한 기능을 위해 현재 요청한 경로인 requestURI를 /login에 쿼리 파라미터로 함께 전달합니다. 물론 /login 컨트롤러에서 로그인 성공 시 해당 경로로 이동하는 기능은 추가로 개발해야 합니다.
4. return;
여기가 중요합니다. 필터를 더는 진행하지 않습니다. 이후 필터는 물론 서블릿, 컨트롤러가 더는 호출되지 않습니다. 앞서 redirect를 사용했기 때문에 redirect 가 응답으로 적용되고 요청이 끝납니다.
WebConfig - loginCheckFilter() 추가
@Bean public FilterRegistrationBean loginCheckFilter() { FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(); filterRegistrationBean.setFilter(new LoginCheckFilter()); filterRegistrationBean.setOrder(2); filterRegistrationBean.addUrlPatterns("/*"); return filterRegistrationBean; }
RedirectURL 처리
로그인에 성공하면 처음 요청한 URL로 이동하는 기능을 개발해보겠습니다.
LoginController - loginV4()
/** * 로그인 이후 redirect 처리 */ @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()); log.info("login? {}", loginMember); if (loginMember == null) { bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다."); return "login/loginForm"; } //로그인 성공 처리 //세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성 HttpSession session = request.getSession(); //세션에 로그인 회원 정보 보관 session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember); //redirectURL 적용 return "redirect:" + redirectURL; }
로그인 체크 필터에서 미인증 사용자는 요청 경로를 포함해서 /login에 redirectURL 요청 파라미터를 추가해서 요청했습
니다. 이 값을 사용해서 로그인 성공 시 해당 경로로 고객을 redirect 합니다.
참고
필터에는 다음에 설명할 스프링 인터셉터는 제공하지 않는, 아주 강력한 기능이 있는데 chain.doFilter(request, response); 를 호출해서 다음 필터 또는 서블릿을 호출할 때 request , response를 다른 객체로 바꿀 수 있습니다. ServletRequest , ServletResponse를 구현한 다른 객체를 만들어서 넘기면 해당 객체가 다음 필터 또는 서블릿에서 사용됩니다.
다음 포스팅에서는 스프링 인터셉터에 대해 알아보겠습니다!!
'Back-end > Spring' 카테고리의 다른 글
[Spring] 예외 처리(Exception)와 오류 페이지 (0) 2022.04.11 [Spring] 스프링 인터셉터(Spring Interceptor) (0) 2022.04.11 [Spring] 메시지, 국제화 (0) 2022.03.28 [SPRING] PRG(POST/REDIRECT/GET) (0) 2022.03.21 [Spring] HTTP 메시지 컨버터 (0) 2022.03.21