-
Spring MVC - Filter를 이용한 요청 로그 기록과 로그인 체크Spring/Spring MVC 2022. 2. 9. 12:47728x90반응형
Filter를 이용한 요청 로그 기록과 로그인 체크
웹 페이지에서는 특정 사용자에게만 보여주는 페이지 또는 로그인을 한 사용자에게만 보여주는 페이지 등 URL에 따라 접근할 수 있는 조건을 체크해줘야 한다. 만약 URL의 접근 조건을 체크해주지 않으면 일반 사용자가 관리자 페이지로 들어갈 수 있거나 로그인을 하지 않는 사용자가 로그인한 사용자에게만 보이는 페이지(정보 수정 등)의 페이지로 들어갈 수 있는 등의 문제가 발생한다.
따라서 특정 페이지의 조건을 체크해줘야 하는데 컨트롤러에 조건을 체크하는 로직을 하나하나 작성하면 코드 중복이 발생할 뿐만 아니라 나중에 유지보수할 때 해당 로직을 일일이 찾아서 수정해야 하는 번거로움이 생긴다.
특정 페이지의 조건을 체크하는 공통의 관심사(Cross-Cutting Concern)를 해결해기 위해서 서블릿 필터 또는 스프링 인터셉터를 사용하여 처리하고, 필터 및 인터셉터는 HttpServletRequest가 제공하는 HTTP의 헤더와 URL의 정보들을 이용하여 공통의 관심사를 처리해준다.
참고로 스프링 MVC를 사용할 때 서블릿 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 것이 더 편리하다
Filter
서블릿이 제공하는 기술
필터 흐름
HTTP 요청 → WAS → 필터 → 디스패처 서블릿 → 컨트롤러
필터를 이용한 요청 제한
HTTP → WAS → 필터 → 디스패처 서블릿 → 컨트롤러 ex) 로그인 사용자 HTTP → WAS → 필터 (조건에 부합하지 않음, 서블릿 호출 X) ex) 비로그인 사용자
필터에서 해당 조건에 부합하지 않은 요청이라 판단하면 컨트롤러에 접근하기 위한 디스패처 서블릿을 호출하지 않는다.
필터 체인
HTTP 요청 → WAS → 필터1 → 필터2 → 필터3 → 디스패처 서블릿 → 컨트롤러
필터는 체인으로 구성되는데 중간에 필터를 자유롭게 추가할 수 있어 여러 조건을 체크할 수 있다.
필터 인터페이스
- Filter 인터페이스는 다음과 같이 3가지 메서드를 제공한다.
- init() : 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출
- doFilter() : 고객의 요청이 올 때마다 해당 메서드가 호출, 필터의 로직(조건 체크)을 구현하는 곳
- destroy() : 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출
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() {} }
Filter를 이용한 요청 로그 기록
로그 필터 구현
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; 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); } }
▶ HTTP 요청이 올 때마다 doFilter 메서드가 호출된다.
▶ HttpServletRequest httpRequest = (HttpServletRequest) request
- ServletRequest request는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스
- HTTP 헤더와 URL 정보를 이용하기 위해 다운 캐스팅을 해준다.
▶ String uuid = UUID.randomUUID().toString()
- HTTP 요청을 구분하기 위한 로그 기록을 위해 요청당 임의의 uuid 생성▶ log.info("REQUEST [{}][{}]", uuid, requestURI)
- 요청에 따른 uuid와 요청 URL 로그 출력
▶ chain.doFilter(request, response)
- 다음 필터가 있으면 필터를 호출하고 없으면 서블릿을 호출한다.
- 이 로직이 없으면 다음 단계로 진행되지 않는다.더보기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("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; 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"); } }
필터 등록 및 설정
- 구현한 필터를 사용하기 위해서는 컨테이너에 빈 등록을 해야 한다.
@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 패턴을 지정
- 한번에 여러 패턴 지정 가능( addUrlPatterns("/url1", "/url2") )
참고
@ServletComponentScan,
@WebFilter(filterName = "필터구현체", urlPatterns = "URL")
을 필터 구현체에 붙여서 사용해도 되지만 필터 순서를 지정할 수 없다.
Filter를 이용한 로그인 인증 처리
로그인 인증 필터 구현
@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); } }
▶ private static final String[] whitelist
- 필터를 적용 제외 URL 모음
▶ if (isLoginCheckPath(requestURI)) {
- 화이트 리스트를 제외한 모든 경우에 로그인 인증 처리
▶ httpResponse.sendRedirect("/login?redirectURL=" + requestURI)
- 비로그인 사용자를 로그인 화면으로 리다이렉트 처리
- 로그인 화면을 처리하는 컨트롤러에서 해당 URL을 받아 로그인 시 해당 URL로 이동 처리
해주기 위해 requestURL을 쿼리 파라미터로 전달▶ return
- 필터를 더이상 진행시키지 않고 반환
- redirect를 이용하여 새로운 요청(/login?redirectURL=requestURI)으로 해당 필터 적용
▶ chain.doFilter(request, response)
- 다음 필터가 있으면 필터를 호출하고 없으면 서블릿을 호출한다.
- 이 로직이 없으면 다음 단계로 진행되지 않는다.
필터 등록 및 설정
@Configuration public class WebConfig implements WebMvcConfigurer { @Bean public FilterRegistrationBean loginCheckFilter() { FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(); filterRegistrationBean.setFilter(new LoginCheckFilter()); filterRegistrationBean.setOrder(2); filterRegistrationBean.addUrlPatterns("/*"); return filterRegistrationBean; } }
▶ FilterRegistrationBean
- 스프링 부트 사용 시 필터 등록을 위한 클래스▶ setFilter(new LogFilter())
- 등록할 필터 지정
▶ setOrder(2)
- 필터의 순서를 지정(높을수록 우선순위가 높다)
▶ addUrlPatterns("/*")
- 필터에 적용할 URL 패턴을 지정
- 한번에 여러 패턴 지정 가능( addUrlPatterns("/url1", "/url2") )
위 두개의 필터를 적용시켰을때 흐름은 다음과 같다.
- URL 요청 : localhost:8080/item
- item 페이지는 로그인 이후에 이용할 수 있는 페이지 - LogFilter의 doFilter 메서드 실행
- 필터의 우선 순위가 가장 높은 LogFilter가 먼저 호출
- log.info("REQUEST [{}][{}]", uuid, requestURI) 로그 출력
- chain.doFilter(request, responsse) 로 다음 필터 호출 - LoginCheckFilter의 doFilter 메서드 실행
- 다음 우선 순위가 높은 LoginCheckFilter 호출
- 로그인 세션이 있는지 확인후 없으면 /login으로 redirect - redirect로 인한 새로운 URL 요청 : localhost:8080/login?redirectURL=requestURI
- LogFilter의 doFilter 메서드 실행
- 필터의 우선 순위가 가장 높은 LogFilter가 먼저 호출
- log.info("REQUEST [{}][{}]", uuid, requestURI) 로그 출력
- chain.doFilter(request, responsse) 로 다음 필터 호출 - LoginCheckFilter의 doFilter 메서드 실행
- 다음 우선 순위가 높은 LoginCheckFilter 호출
- 해당 필터 제외 URL인 /login이므로 로그인 확인 체크 제외
- chain.doFilter(request, responsse) 로 URL요청에 매핑된 컨트롤러 호출 - 로그인 성공시 처음 요청했던 URL로 이동
728x90반응형'Spring > Spring MVC' 카테고리의 다른 글
Spring MVC - ArgumentResolver을 활용한 애노테이션 만들기 (0) 2022.02.09 Spring MVC - interceptor를 이용한 요청 로그 기록과 로그인 체크 (0) 2022.02.09 Spring MVC - Session을 이용한 로그인 처리 (HttpSession, @SessionAttribute), 세션 정보 조회 (0) 2022.02.08 Spring MVC - Session을 직접 구현하여 로그인 처리 (0) 2022.02.08 Spring MVC - Cookie를 이용한 로그인 처리와 문제점 (0) 2022.02.08