chanhee01 2023. 3. 1. 15:42

로그인이 된 사용자만 상품 관리 페이지에 들어갈 수 있어야 하는데 로그인을 하지 않은 사용자가 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;
    }
우리가 만든 필터들이 적용될 수 있도록 하는 @Configuraiton이다. logFilter를 첫 번째 체인으로, loginCheckFilter를 두 번째 체인으로 지정했다.
 
 
 
 
LoginController
@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 화면으로 넘어가게 된다.