로그인 처리2 - 필터, 인터셉터

rooni97·2022년 9월 28일
0

Spring MVC2

목록 보기
2/2

서블릿 필터 - 소개

어플리케이션 여러 로직에서 공통으로 관심있는 것을 공통 관심사라고 한다.

AOP로도 해결할 수 있지만 웹과 관련한 공통 관심사 처리는 HTTP 헤더, URL 정보 등이 필요하므로 HttpServletRequest를 제공하는 서블릿 필터 or 스프링 인터셉터를 사용하자!

필터 흐름

HTTP 요청 -> WAS -> 필터 -> 서블릿(디스패처 서블릿) -> 컨트롤러

참고로 필터는 특정 URL 패턴에 적용할 수 있다.
ex) /*: 모든 요청에 필터 적용

필터 제한

로그인 유저: HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
비 로그인: HTTP 요청 -> WAS -> 필터 (적절하지 않은 요청.. 서블릿 호출 X)

필터는 이런 식으로 로그인 여부를 체크하기에 좋다.

필터 체인

HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러

필터는 체인으로 구성되어 중간에 자유롭게 추가할 수 있다.

필터 인터페이스

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() {}
    
}

필터 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리한다.

init(): 필터 초기화 메서드로 서블릿 컨테이너가 생성될 때 호출
doFilter(): 고객 요청마다 호출되는 메서드로 필터의 로직
destroy(): 필터 종료 메서드로 서블릿 컨테이너가 종료될 때 호출


서블릿 필터 - 요청 로그

chain.doFilter(request, response)

  • 다음 필터가 있으면 호출, 없으면 서블릿을 호출
  • 호출하지 않으면 다음 단계로 넘어가지 않음

WebConfig - 필터 설정

FilterRegistrationBean을 사용한다.

  • setFilter(new LogFilter()): 등록할 필터(LogFilter)를 지정
  • setOrder(1): 필터는 체인이라 순서 필요, 낮을 수록 먼저 동작
  • addUrlPatterns("/*"): 필터를 적용할 URL 패턴 지정, 한 번에 여러 패턴도 지정 가능

참고

  • @ServletComponentScan +
    @WebFilter(filterName = "logFilter", urlPatterns = "/*")는 필터 등록은 되는데 순서 조절 불가능
  • 같은 요청 로그에 같은 식별자 남기는 법: logback mdc

서블릿 필터 - 인증 체크

@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);
                    httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
                    return;
                }
            }

            chain.doFilter(request, response);

        } catch (Exception e) {
            throw e;
        } finally {
            log.info("인증 체크 필터 종료 {}", requestURI);
        }
    }

    private boolean isLoginCheckPath(String requestURI) {
        return !PatternMatchUtils.simpleMatch(whiteList, requestURI);
    }

}

whiteList = {...}

  • 인증 필터를 거쳐도 접근을 허용해줘야 하는 리소스를 지정
  • 화이트 리스트 경로는 인증과 무관하게 항상 허용

isLoginCheckPath()

  • 화이트 리스트 제외한 경로는 인증 체크

httpResponse.sendRedirect("/login?redirectURL=" + requestURI)

  • 로그인하면 접근했던 경로로 다시 들어가기 위함
  • 상품 관리 -> 로그인 화면 -> 로그인 -> 상품 관리 (홈 화면X)

return

  • 미인증 시 필터는 다음 필터(혹은 서블릿)부터 호출 X

스프링 인터셉터 - 소개

서블릿 필터는 서블릿이 제공, 인터셉터는 스프링 MVC가 제공한다.

스프링 인터셉터 흐름

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
  • 스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이, 컨트롤러 호출 직전에 호출된다.
  • MVC 제공 기능이므로 MVC 시작점인 디스패처 서블릿 이후에 등장
  • 필터와 달리 URL 패턴을 정밀하게 설정 가능

스프링 인터셉터 제한

로그인 유저: HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
비로그인: HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터 (적절하지 않은 요청.. 컨트롤러 호출 X)

스프링 인터셉터 체인

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러
  • 필터와 호출 순서만 다르고 기능은 비슷함
  • 인터셉터는 필터보다 편리하고 정교하고 다양한 기능을 제공

스프링 인터셉터 인터페이스

public interface HandlerInterceptor {
    
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {}
    
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {}
    
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {}
}

preHandle

  • 핸들러 어댑터 호출 전에 호출
  • 반환값이 true면 다음으로 진행
  • false면 다음 인터셉터 or 핸들러어댑터로 진행 X

postHandle

  • 핸들러 어댑터 호출 후에 호출

afterCompletion

  • 뷰 렌더링 이후 호출

스프링 인터셉터 예외

예외 발생 시

  • postHandle()은 호출되지 않음
  • afterCompletion()은 항상 호출 + 예외 정보도 포함되어 호출됨
  • 예외와 무관하게 공통 처리를 하려면 afterCompletion() 사용

스프링 인터셉터 - 요청 로그

request.setAttribute(LOG_ID, uuid)

  • preHandle에서 지정한 UUID를 postHandle, afterCompletion에서 사용하기 위해 담아줌
  • return true: 다음 인터셉터 or 컨트롤러 호출
  • 예외가 발생해도 호출하도록 종료 로그를 afterCompletion에서 실행

WebConfig - 인터셉터 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error");
    }
}
  • registry.addInterceptor(new ...): 인터셉터를 등록
  • order(1): 인터셉터 호출 순서
  • addPathPatterns("/**"): 인터셉터 적용할 URL 패턴
  • excludePathPatterns(...): 인터셉터 제외할 패턴

PathPattern 공식 문서 참고
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html


스프링 인터셉터 - 인증 체크

LoginCheckInterceptor

@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {

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

        String requestURI = request.getRequestURI();

        log.info("인증 체크 인터셉터 실행 {}", requestURI);
        HttpSession session = request.getSession(false);

        if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
            log.info("미인증 사용자 요청");
            response.sendRedirect("/login?redirectURL=" + requestURI);
            return false;
        }


        return true;
    }

}

순서, 세밀한 설정

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error");

        registry.addInterceptor(new LoginCheckInterceptor())
                .order(2)
                .addPathPatterns("/**")
                .excludePathPatterns("/", "/members/add", "/login", "logout",
                        "/css/**", "/*.ico", "/error");
    }
}

ArgumentResolver 활용

@Login이 있으면 ArgumentResolver가 동작하여
자동으로 세션에서 로그인 회원을 찾고, 없으면 null을 반환

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
  • @Target(ElementType.PARAMETER): 파라미터에만 사용
  • @Retention(RetentionPolicy.RUNTIME): 리플렉션 등을 사용할 수 있도록 런타임까지 어노테이션 정보가 남아있음

LoginMemberArgumentResolver 생성

HandlerMethodArgumentResolver를 구현해보자.

@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        log.info("supportsParameter 실행");

        boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
        boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());

        return hasLoginAnnotation && hasMemberType;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        log.info("resolveArgument 실행");

        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        HttpSession session = request.getSession(false);
        if (session == null) {
            return null;
        }

        return session.getAttribute(SessionConst.LOGIN_MEMBER);
    }
}
  • supportsParameter(): @Login 어노테이션이 있으면서 Member 타입이면 해당 ArgumentResolver 사용
  • resolverArgument(): 컨트롤러 호출 직전 파라미터 정보를 생성, 세션에 있는 member 객체를 찾아 반환하고 컨트롤러 메서드를 호출하면서 파라미터로 전달된다.

WebMvcConfigurer에 설정 추가

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new LoginMemberArgumentResolver());
    }
}

ArgumentResolver를 활용하여 공통 작업이 필요할 때 컨트롤러를 더욱 편리하게 사용 가능!

0개의 댓글