스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 섹션7. 로그인 처리2 - 필터, 인터셉터
CS/김영한 스프링 강의

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 섹션7. 로그인 처리2 - 필터, 인터셉터

필터 전에 공통 관심사라는 개념을 이해해야 하는데, 컨트롤러를 만들고 요청을 처리하지만 로그인이 되어있다고 가정하고 요청을 처리하는 서비스가 많을 것이다. 이런 것들은 전에 했던 것처럼 일일히 다 코드로 넣기는 힘드니 컨트롤러 서비스 처리 함수로 가기 전에 어딘가를 공통적으로 들른 후 가게 하는게 좋다.

예전에 수문장이라고 배운게 있지만 HTTP 요청은 이 요청용 필터로 쓰라고 만든 게 있다. 이걸 써서 로그인 했으면 로그인 로직 처리해서 넘기고 아니면 거절하고 하는 걸 사용할거다.

진짜 filter를 하는군 doFilter라서 이것만 보면 된다. 그냥 ServletRequest는 기능이 별로 없고 어차피 거의 Http쓸 거라서 그냥 저렇게 캐스팅으로 바꿔도 된다. chain.doFilter를 통해 들어온 요청을 해당 함수로 넘겨준다.

물론 config를 만들어서 등록해야 사용할 수 있으며, spring boot를 사용했을 때 @Bean으로 등록할 수 있다.

실제로는 각 http 요청 단위마다 묶어서 남기는 logback mdc라는걸 쓴다고 한다.

 

이제 수문장용으로 filter 만들었으니까 로그인 안한 사용자가 있으면 안되는 url들을 돌려보낼 때 적용하면 편리할 것이다.

package hello.login.web.filter;

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 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);

                String loginId = (String) httpRequest.getSession().getAttribute(SessionConst.LOGIN_MEMBER);

                if (loginId == 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);
    }
}

볼만한건 아이디를 못 얻어왔을 시, chain.doFilter조차 하지 않고 그냥 return한 것. 물론 아무런 http 응답 없이 보내면 안되니 로그인 페이지로 redirect 해준다. 또 어느 url로 접속을 시도했는지 query 형태로 기억해서 활용할거다.

 

만약 로그인 하지 않은 상태에서 로그인이 필요한 url에 접속했을 경우 저렇게 query 형태로 기억한다. 로그인 성공하면 위에서 설정한 대로 redirect 해준다.

 

 

다음에 쓸건 스프링 인터셉터인데, 필터는 서블릿 전에 실행되었지만 스프링 인터셉터는 서블릿 실행 후 컨트롤러로 가기 전에 실행된다. 그래서 더 여러가지를 할 수 있단다.

사용 방법 및 등록 방법은 필터랑 조금 다르다.

 

package hello.login.web.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    public static final String LOG_ID = "logId";

    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response, 
            Object handler) throws Exception {

        String requestURI = request.getRequestURI();
        String uuid = UUID.randomUUID().toString();

        request.setAttribute(LOG_ID, uuid);

        // @RequestMapping: HandlerMethod
        // 정적 리소스: ResourceHttpRequestHandler
        if (handler instanceof HandlerInterceptor) {
            HandlerMethod hm = (HandlerMethod) handler; // 호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
        }

        log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
        return true; // false를 반환하면 뒤에 있는 인터셉터나 핸들러로 요청이 안 넘어감. 예외를 던지는 것과 같다.

    }

    @Override
    public void postHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler,
            ModelAndView modelAndView
    ) throws Exception {
        log.info("postHandle [{}]", modelAndView);
    }

    @Override
    public void afterCompletion(
            HttpServletRequest request, 
            HttpServletResponse response,
            Object handler, Exception ex
    ) throws Exception {
        String requestURI = request.getRequestURI();
        Object logId = request.getAttribute(LOG_ID);
        log.info("RESPONSE [{}][{}][{}]", logId, requestURI, handler);

        if (ex != null) {
            log.error("afterCompletion error!!", ex);
        }

    }
}

정의한 뒤 WebMvcConfigurer를 상속해서 override 해야 한다.

 

 

PathPattern (Spring Framework 6.0.11 API)

 

PathPattern (Spring Framework 6.0.11 API)

Compare this pattern with a supplied pattern: return -1,0,+1 if this pattern is more specific, the same or less specific than the supplied pattern.

docs.spring.io

필터로 한 것에 비해 편리하게 할 수 있다. 

 

 

앞 필터에서 whilelist 만들고 urlmatchutil로 맞느지 아닌지 점검하고.. 했던 것들을 그냥 할 수 있다.

 

 

ArgumentResolver는 보너스라고 하는데, @SessionAttribute로 해서 가져오는 이런 공통 사항을 더 편리하게 할 수 있다고 한다.

 

 

지금까지 로그인 관련 기능으로 서버에 공통관심 사항을 처리하는 방법에 대해 알아보았다.