서블릿 필터 실습해보기

sion Jeong·2024년 6월 27일
0

여기서 부턴 그냥 실습부분입니다.

서블릿 필터 구현해보기 (요청로그 찍기)

필터가 정말 수문장 역할을 잘 하는지 확인해보기 위해. 가장 단순 필터인 모든 요청 로그를 남기는 필터를 개발하고 적용해보자

필터 구현 클래스 생성

    @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(); //요청 URI를 담기위한 변수
           
            String uuid = UUID.randomUUID().toString(); //HTTP요청을 구분하기 위한 임의의 uuid 담을 변수
           
            try {
                log.info("REQUEST [{}][{}]", uuid, requestURI); //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");
        }
    }
  • public class LogFilter implements Filter {}
    • 필터를 사용하려면 필터 인터페이스를 구현해야 한다. (구현 클래스 만들기)
  • doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    • HTTP 요청이 오면 doFilter 가 호출된다.
    • ServletRequest 는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스이다. 따라서 다운케스팅 해줘야함
  • HttpServletRequest httpRequest = (HttpServletRequest) request;
    • ServletRequest 는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스이다.
    • 따라서 기능이 별로 없기 떄문에 (HttpServletRequest) request; 와 같이 다운 케스팅 해서 사용하면된다.
  • String requestURI = httpRequest.getRequestURI();
    • 요청 URI 를 담기위한 변수
  • String uuid = UUID.randomUUID().toString();
    • HTTP 요청을 구분하기 위해 요청당 임의의 uuid 를 생성해두고 담아둘 변수
  • log.info("REQUEST [{}][{}]", uuid, requestURI);
    • uuid 와 requestURI 를 출력한다.
    • 모든 요청에 로그를 찍는 핵심 로직이 돌아가는 부분
  • chain.doFilter(request, response);
    • 이 부분이 가장 중요!
    • 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다.
    • 이 로직을 호출하지 않으면 다음 단계로 진행되지 않는다 (다음 필터로 넘어가라, 서블릿으로 가라 명령이 없는거니깐)

필터 경로 설정, 등록하기

    @Configuration
    public class WebConfig {

        @Bean //WAS가 서버 띄우면서 -> 필터를 스프링 빈에 등록해줌
        public FilterRegistrationBean logFilter() {
            FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
            filterRegistrationBean.setFilter(new LogFilter()); //등록할 필터 지정
            filterRegistrationBean.setOrder(1); //필터의 우선순위 지정
            filterRegistrationBean.addUrlPatterns("/*"); //필터를 적용할 URL
            return filterRegistrationBean;
        }
    }

필터를 등록하는 방법은 여러가지가 있지만, 스프링 부트를 사용한다면 FilterRegistrationBean 을 사용해서 등록하면 된다.

  • setFilter(new LogFilter())
    • 등록할 필터를 지정한다
  • setOrder(1)
    • 필터는 체인으로 동작한다. 따라서 순서가 필요하다. 낮을 수록 먼저 동작한다.
  • addUrlPatterns("/*")
    • 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다.

참고

@ServletComponentScan @WebFilter(filterName = "logFilter", urlPatterns = "/*")

로 필터 등록이 가능하지만 필터 순서 조절이 안된다. 따라서 FilterRegistrationBean 을 사용하자.

실무에서 HTTP 요청시 같은 요청의 로그에 모두 같은 식별자를 자동으로 남기는 방법은 logback mdc로 검색해보자.

실행 로그
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 을 /* 로 등록했기 때문에 모든 요청에 해당 필터가 적용된다.


서블릿 필터 - 인증 체크 (필터로 자주쓰는 기능)

이번엔 서블릿 필터(수문장)기능을 사용해 로그인 인증을 체크하는 인증 체크 필터를 개발해보자.

로그인 되지 않은 사용자는 상품 관리 뿐만 아니라 → 미래에 개발될 페이지에도 접근하지 못하도록 하자. (필터를 적용할 URL을 설정해 주면됨)

  1. 필터 추가하기 (필터 인터페이스 구현체 추가)
    @Slf4j
    public class LoginCheckFilter implements Filter {
        
        //비 로그인시에도 접근할 수 있는 URL경로를 변수에 담아줌
        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(); //요청 URI를 담기위한 변수
         
            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) {//세션이 null 이거나 로그인 데이터가 없다.
                        log.info("미인증 사용자 요청 {}", requestURI);//로그(미인증 사용자가 들어온 거다)
                        
                        //로그인으로 redirect 시킴
                        httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
                        return; //여기가 중요, 미인증 사용자는 다음으로 진행하지 않고 끝!
                        
                    }
                }
                //화이트리스트면 위의 인증을 탈 필요가 없다 -> chain.doFilter로 다음 필터로 넘어가자
                chain.doFilter(request, response); //다음 필터로 가거나, 서블릿 요청으로 ㄱㄱ
                
            } catch (Exception e) {
                throw e; //예외 로깅 가능 하지만, 톰캣까지 예외를 보내주어야 함
            } finally {
                log.info("인증 체크 필터 종료 {}", requestURI); //로그찍기
            }
        }

        /**
         * 화이트 리스트의 경우 인증 체크X 메서드
         */
        private boolean isLoginCheckPath(String requestURI) {
            return !PatternMatchUtils.simpleMatch(whitelist, requestURI);//!라 로그인 체크해야하는 경로다 알려주는 용도
        }
    }
  • doFilter만 구현됐지? 인터페이스인데?
    • init()과 destory()메서드는 디폴트로 구현되어있어서 오버라이드 안해도됨
    • 그리고 이전에 우선순위 앞선 필터가 필터를 켜놓은 상태기 때문에 또 킬 필요없다.
  • whitelist = {"/", "/members/add", "/login", "/logout","/css/*"};
    • 인증 필터를 적용해도 홈, 회원가입, 로그인 화면, css 같은 리소스에는 비로그인시에도 접근할 수 있어야 한다.
    • 이렇게 화이트 리스트 경로는 인증과 무관하게 항상 허용한다. 화이트 리스트를 제외한 나머지 모든 경로에는 인증 체크 로직을 적용한다.
  • isLoginCheckPath(requestURI)
    • 화이트 리스트를 제외한 모든 경우에 인증 체크 로직을 적용한다.
  • httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
    • 기능: 미인증 사용자는 로그인 화면으로 리다이렉트 한다
    • 그런데 로그인 이후에 다시 홈으로 이동해버리면, 원하는 경로를 다시 찾아가야 하는 불편함이 있다.
    • 예를 들어서 상품 관리 화면을 보려고 들어갔다가 로그인 화면으로 이동하면, 로그인 이후에 다시 상품 관리 화면으로 들어가는 것이 좋다. 이런 부분이 개발자 입장에서는 좀 귀찮을 수 있어도 사용자 입장으로 보면 편리한 기능이다.
    • 이러한 기능을 위해 현재 요청한 경로인requestURI 를 /login 에 쿼리 파라미터로 함께 전달한다.
    • 물론 /login 컨트롤러에서 로그인 성공시 해당 경로로 이동하는 기능은 추가로 개발해야 한다.
  • return;
    • 여기가 중요하다. 필터를 더는 진행하지 않는다.
      (미인증 사용자라면, 더이상 chain.doFilter 다음필터로 넘어가기 하진 않는다)
    • 이후 필터는 물론 서블릿, 컨트롤러가 더는 호출되지 않는다. 앞서 redirect 를 사용했기 때문에 redirect 가 응답으로 적용되고 요청이 끝난다.
    1. 필터 경로 설정, 등록하기

          @Bean//WAS가 서버 띄우면서 -> 필터를 스프링 빈에 등록해줌
          public FilterRegistrationBean loginCheckFilter() {
              FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
              filterRegistrationBean.setFilter(new LoginCheckFilter()); //등록할 필터 지정
              filterRegistrationBean.setOrder(2); //필터의 우선순위 지정
              filterRegistrationBean.addUrlPatterns("/*");//필터를 적용할 URL 
             
              return filterRegistrationBean;
          }
  • setFilter(new LoginCheckFilter())
    • 등록할 필터를 지정한다 (로그인 필터)
  • setOrder(2)
    • 필터는 체인으로 동작한다. 따라서 순서가 필요하다. 낮을 수록 먼저 동작한다.
    • 순서를 2번으로 잡았다. 로그 필터 다음에 로그인 필터가 적용된다.
  • addUrlPatterns("/*")
    • 필터를 적용할 URL 패턴을 지정한다
    • 모든 요청에 로그인 필터를 적용한다. ( 따라서 미래에 생길 컨트롤러도 다 필터가 적용된다)
    • 화이트 리스트는 필터가 적용되지 않는다.

RedirectURL 처리

로그인에 성공하면 처음 요청한 URL로 이동하는 기능을 개발해보자

LoginController 
    /**
     * 로그인 이후 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; //로그인 성공시 처음 요청한 URL로 이동
    }

정리

서블릿 필터를 잘 사용한 덕분에 로그인 하지 않은 사용자는 나머지 경로에 들어갈 수 없게 되었다. 공통 관심사를 서블릿 필터를 사용해서 해결한 덕분에 향후 로그인 관련 정책이 변경되어도 필터 부분만 변경하면 된다.

profile
개발응애입니다.

0개의 댓글