[Spring] 필터, 인터셉트

hi·2022년 12월 14일
0

공통 관심사 (cross-cutting concern)
: 애플리케이션 여러 로직에서 공통으로 관심이 있는 것

웹과 관련된 공통 관심사를 처리할 때는 HTTP 헤더나 URL 정보가 필요한데,
서블릿 필터와 스프링 인터셉터는 HttpServletRequest를 제공한다

서블릿 필터

: 서블릿이 지원하는 수문장

필터 흐름

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

  • 특정 URL 패턴에 적용 가능
    ex. 모든 요청에 적용시 /*

  • 스프링을 사용하는 경우 서블릿은 디스패처 서블릿

  • 필터에서 제한이 걸리면 바로 끝낼 수 있다
    ex. 로그인 여부 체크

  • 체인으로 구성되며 자유롭게 추가 가능
    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()

  • 고객의 요청이 올 때 마다 호출 (UUID로 요청 구분)
    참고) 같은 요청의 로그에 모두 같은 식별자를 자동으로 남기려면 logback mdc
  • 필터의 로직을 구현하는 부분
  • ServletRequest request : HTTP 요청이 아닌 경우까지 고려, HTTP 사용시 다운캐스팅하여 사용
  • FilterChain chain : 다음 단계로 진행
    chain.doFilter(request, response); 를 사용하여 다음 필터가 있으면 호출, 없으면 서블릿 호출 (request, response는 다른 객체로 변경 가능)

destroy()

  • 필터 종료 메서드
  • 서블릿 컨테이너가 종료될 때 호출

필터 등록

  • 스프링 부트 사용시 FilterRegistrationBean을 사용하여 등록

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

@Configuration
public class WebConfig {

	@Bean
	public FilterRegistrationBean logFilter() {
		
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
		filterRegistrationBean.setFilter(new LogFilter());
		filterRegistrationBean.setOrder(1);
		filterRegistrationBean.addUrlPatterns("/*");
		return filterRegistrationBean;
	}
}

로그인 인증 체크 필터 예시

@Slf4j
public class LoginCheckFilter implements Filter {

	//whitelist를 제외한 모든 경로에 인증 체크 로직 적용
	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);
 	}
}
  • httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
    미인증 사용자를 로그인 화면으로 리다이렉트 -> 로그인 후, 기존 페이지로 이동

스프링 인터셉터

: 스프링 MVC가 제공하는 기술

스프링 인터셉터 흐름

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러

  • 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출

  • URL 패턴 적용 가능
    (서블릿 URL 패턴과는 다르고, 매우 정밀하게 설정 가능)

  • 필터에서 제한이 걸리면 바로 끝낼 수 있다
    ex. 로그인 여부 체크

  • 체인으로 구성되며 자유롭게 추가 가능


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

스프링의 인터셉터를 사용하려면 HandlerInterceptor 인터페이스를 구현


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 {}
}
  • 서블릿 필터 : doFilter() 하나만 제공
    인터셉터 : 컨트롤러 호출 전 (preHandle) , 호출 후 (postHandle) , 요청 완료 이후 (afterCompletion) 와 같이 단계적으로 세분화

  • 서블릿 필터 : request , response 만 제공
    인터셉터 : 호출되는 컨트롤러(handler) 정보 제공, 어떤 modelAndView 가 반환되는지 응답 정보 제공


필터 vs 인터셉터 ?
인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공
스프링 MVC를 사용하며, 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 것이 더 편리


핸들러 정보

HandlerMethod : @Controller, @RequestMapping 사용 시
ResourceHttpRequestHandler : /resources/static 와 같은 정적 리소스 사용 시


스프링 인터셉터 호출 흐름

정상 흐름

스프링 인터셉터 예외 상황

postHandle : 컨트롤러에서 예외가 발생하면 호출되지 않음
afterCompletion : 예외 발생시에도 항상 호출, 예외(ex)를 파라미터로 받아 로그 출력 가능


인터셉터 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {
	
    @Override
 	public void addInterceptors(InterceptorRegistry registry) {

		registry.addInterceptor(new LogInterceptor()) //인터셉터 등록
				.order(1) 							  //호출 순서 지정
				.addPathPatterns("/**")				  //적용 URL 패턴 지정
				.excludePathPatterns("/css/**", "/*.ico", "/error"); //제외 패턴 지정
 	}
 	//...
}
  • WebMvcConfigurer가 제공하는 addInterceptors()를 사용하여 인터셉터 등록

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


로그인 인증 체크 필터 예시

@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("미인증 사용자 요청");
 			//로그인으로 redirect
 			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 활용

해당 기능을 사용하여 로그인 회원을 편리하게 찾을 수 있다

@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {

	//세션에 회원 데이터가 없으면 home
    if (loginMember == null) {
    	return "home";
    }

    //세션이 유지되면 로그인으로 이동
    model.addAttribute("member", loginMember);
    return "loginHome";
}

@Login 애노테이션을 생성하여
애노테이션이 있으면 ArgumentResolver가 동작해서 자동으로 세션에 있는 로그인
회원을 찾아주고, 없다면 null 을 반환하도록 개발

@Target(ElementType.PARAMETER) //파라미터에만 사용
@Retention(RetentionPolicy.RUNTIME) //런타임까지 애노테이션 정보가 남아있음 -> 리플렉션 등을 활용 가능
public @interface Login {
}

HandlerMethodArgumentResolver 구현

@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {


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

	   	//파라미터에 Login 애노테이션이 있는지 확인
       	boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class); 
       	//파라미터 타입이 Member인지 확인
    	boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());

        return hasLoginAnnotation && hasMemberType; //둘 다 true => resolveArgument 실행
    }

    @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;
        }

        Object member = session.getAttribute(SessionConst.LOGIN_MEMBER);

        return member;
    }
}

LoginMemberArgumentResolver 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

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

ArgumentResolver를 활용하면 공통 작업 시, 컨트롤러를 더욱 편리하게 사용 가능

0개의 댓글