[Spring_Boot] 필터, 인터셉터

최현석·2022년 12월 1일
0

Spring_Boot

목록 보기
15/31
post-thumbnail

🧩 서블릿 필터 vs 스프링 인터셉터

흐름

  • 필터를 적용하면 필터가 호출된 이후 서블릿이 호출된다.
    (여기서 서블릿은 스프링의 경우 디스패처 서블릿을 의미한다고 생각하면 된다.)

  • 인터셉터를 적용하면 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출된다.

  • 필터는 서블릿 호출전에 인터셉터는 서블릿 호출 이후 호출되기에 인터셉터는 서블릿에서 예외가 발생한다면 호출되지 않는다.



체인

  • 둘 다 자유롭게 필터 및 인터셉터를 추가할 수 있다.
  • 로그를 남기는 필터(혹은 인터셉터)를 적용 후 그 다음 로그인 여부를 체크하는 필터(혹은 인터셉터)를 만들어 적용할 수 있다.



🧩 필터

  • 필터
    HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
  • 필터는 말 그대로 요청과 응답을 거른뒤 정제하는 역할을 한다.
  • Dispatcher Servlet에 요청이 전달되기 전 / 후에 url 패턴에 맞는 모든 요청에 대해 부가 작업을 처리할 수 있는 기능을 제공한다.
  • 즉, 스프링 컨테이너가 아닌 톰캣과 같은 웹 컨테이너에 의해 관리가 되는 것이고, 스프링 범위 밖에서 처리되는 것이다.
  • 필터 체인
    HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 ->서블릿 -> 컨트롤러

필터(Filter)의 메소드 종류

init() :

  • 필터 초기화 메서드로 서블릿 컨테이너가 생성될 때 호출된다.
  • 필터 객체를 초기화하고 서비스에 추가하기 위한 메소드이다.
  • 웹 컨테이너가 1회 init()을 호출하여 필터 객체를 초기화하면 이후 요청들은 doFilter()를 통해 처리된다.

doFilter() :

  • 고객의 요청이 올 때마다 해당 메서드가 호출된다.
  • url-pattern에 맞는 모든 HTTP 요청이 디스패처 서블릿으로 전달되기 전에 웹 컨테이너에 의해 실행되는 메소드이다.
  • doFilter의 파라미터로 FilterChain이 있는데, FilterChaindoFilter 통해 다음 대상으로 요청을 전달할 수 있게 된다.
  • chain.doFilter() 전, 후에 우리가 필요한 처리 과정을 넣어줌으로써 원하는 처리를 진행할 수 있다.

destroy() :

  • 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.
  • 필터 객체를 제거하고 사용하는 자원을 반환하기 위한 메소드이다.
  • 웹 컨테이너가 1회 destroy()를 호출하여 필터 객체를 종료하면 이후에는 doFilter에 의해 처리되지 않는다.

🧩 로그 필터

LogFilter

  • doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    • HTTP 요청이 오면 doFilter가 호출된다.
    • ServletRequest request는 HTTP 요청이 아닌 경우도 고려해서 만든 인터페이스이다. HTTP를 사용하면 HttpServletRequest로 명시적 형변환을 한 뒤 사용하면 된다.
  • chain.doFilter(request, response);
    • 가장 중요하다. 다음 필터가 있으면 다음 필터를 호출하고 필터가 없으면 서블릿을 호출한다. 만약 이 로직을 호출하지 않으면 다음단계로 진행되지 않는다.
public class LogFilter implements Filter{

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest httpServletRequest = (HttpServletRequest)request;
		String requestURI = httpServletRequest.getRequestURI();
		
		// doFilter 이전
		System.out.println("requestURI : " + requestURI);
		
		chain.doFilter(request,response);
		
		// doFilter 이후
		System.out.println("responseURI : " + requestURI);
	}
	
}

WebConfig

  • 필터를 만들기만한다고 자동으로 등록되지는 않기에 Configuration 을 만들어 해당 필터를 등록한다.
  • setFilter(new LogFilter)
    • 등록 할 필터를 지정한다.
  • setOrder(1)
    • 필터는 체인으로 동작하기에 순서가 필요하다. 순서가 낮을수록 먼저 동작한다.
  • addUrlPatterns("/*")
    • 필터를 적용할 URL 패턴을 지정하며, 하나 이상의 패턴을 지정 할 수도 있다.
  // @Bean
  // postconstruct는 스프링부트에서 제공해주는 어노테이션이였어서 Bean등록도 다 처리가 됬었다
	public FilterRegistrationBean logFilter() {
		FilterRegistrationBean<Filter> fiterRegistrationBean 
			= new FilterRegistrationBean<Filter>();
		fiterRegistrationBean.setFilter(new LogFilter());	// LogFilter 등록
		fiterRegistrationBean.setOrder(1);
		fiterRegistrationBean.addUrlPatterns("/*");			// 모든 url 다 적용
		
		return fiterRegistrationBean;
	}

	
}

🧩 로그인 인증 체크 필터

LoginCheckFilter

  • whitelist로 지정한 경로를 제외하고는 모두 로그인 상태를 검사 후 페이지 접근 여부를 결정한다.

  • private static final String[] whitelist = {"/", "/members/add", ...};

    • 모든 곳에 로그인이 되어있어야 하는건 아니다. 정적 리소스와 로그인, 로그아웃의 경우 로그인을 하지 않아도 접근이 가능해야 한다. 이 화이트리스트를 이용해 이러한 경로 검사를 해 준다.
  • isLoginCheckPath(String requestURI)

    • 매개변수로 전달받은 requestURI가 화이트리스트와 일치하는지 검사한다. 이때 PatternMatchUtils라는 정적 헬퍼 클래스를 이용하여 쉽게 경로 검사가 가능하다.
  • httpResponse.sendRedirect("/login?redirectURL=" + requestURI);

    • 로그인을 안했는데 로그인이 필요한 페이지에 접근시 로그인 페이지로 이동시키는건 알기 쉽다.
      그런데 redirectURL이라고 queryString을 작성하고 있다.
      이게 같이 작성 된 이유는, 예를들어 내가 내가 등록한 상품 목록 페이지에 접근하려는 상황에서 로그인이 안되어 있어 로그인 페이지로 이동했다고 가정할 때 로그인을 하면 어디로 이동시킬 것인가? 별 생각을 하지 않는다면 그냥 메인 페이지를 얘기할 수 있지만, 내가 원래 접근하려는 페이지로 다시 이동시켜주면 사용자 입장에선 편리할 것이다.
      • 로그인 하지 않고 items 입력 후 이동
      • 사용자가 넘겨준 uri를 파라미터 값으로 기억
  • return;
    • 필터는 더는 진행하지 않는다. redirect를 사용했기에 redirect가 응답으로 적용되고 요청이 끝난다.
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;
		
		System.out.println("인증 체크 필터 시작 ");
		if(isLoginCheckpath(requestURI)) {
			System.out.println("인증체크 로직 실행 : " + requestURI);
			HttpSession session = httpRequest.getSession(false);
			if(session == null 
					|| session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
				System.out.println("미 인증 사용자 요청");
				// 로그인으로 redirect
                // 사용자가 넘겨준 uri를 파라미터 값으로 기억
				httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
				// 미인증 사용자는 다음으로 진행하지 않고 끝낸다.
				return;
			}
		}
		
		// 다음 단게로 넘어간다.
		chain.doFilter(request, response);
	}

	/*
	 * 화이트 리스트의 경우 인증 체크 X
	 * PatternMatchUtils : 파라미터 문자열이 특정 패턴에 매칭되는지를 검사함.
	 */
	private boolean isLoginCheckpath(String requestURI) {
		return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
	}
}

WebConfig

  • 작성한 로그인 인증 검사 필터를 설정에 등록
	@Bean
	public FilterRegistrationBean loginCheckFilter() {
		FilterRegistrationBean<Filter> fiterRegistrationBean 
			= new FilterRegistrationBean<Filter>();
		fiterRegistrationBean.setFilter(new LoginCheckFilter()); // LoginCheckFilter 등록
		fiterRegistrationBean.setOrder(2);
		fiterRegistrationBean.addUrlPatterns("/*");				// 모든 url 다 적용
		
		return fiterRegistrationBean;
	}

LoginController

  • 로그인이 성공했을 경우 redirectURL 이라는 @RequestParam을 조회해 만약 다른 페이지로 접근을 시도하다 로그인 페이지로 온 경우 다시 되돌아가기위해 사용한다.

  • return "redirect:" + redirectURL;

    • 파라미터로 넘어온 값이 없으면 /
    • 파라미터로 넘어온 값이 있으면 /items
	@PostMapping("/login")
	public String loginV3(@ModelAttribute LoginForm form, Model model, 
			RedirectAttributes redirectAttributes, HttpServletRequest request,
			@RequestParam(defaultValue = "/")String redirectURL) {
		
		Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
		if(loginMember == null) {
			// 로그인 실패
			model.addAttribute("msg", "로그인 실패");
			return "login/loginForm";
		}
		
		// 로그인 성공
		HttpSession session = request.getSession();
		// 세션에 로그인 회원 정보 보관
		session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
		redirectAttributes.addFlashAttribute("msg","로그인성공");
        // 파라미터로 넘어온 값이 없으면 /
		// 파라미터로 넘어온 값이 있으면 /items
		return "redirect:" + redirectURL;
		
	}


🧩 인터셉터

  • 스프링 인터셉터도 서블릿 필터와 같이 웹과 관련된 공통 관심 사항을
    효과적으로 해결할 수 있는 기술이다.
  • 서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링 MVC가 제공하는 기술이다.
  • 둘다 웹과 관련된 공통 관심 사항을 처리하지만, 적용되는 순서와 범위,
    그리고 사용방법이 다르다.
  • HTTP요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
  • 스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출
    직전에 호출된다.
  • 스프링 인터셉터는 스프링 MVC가 제공하는 기능이기 때문에 결국
    디스패처 서블릿 이후에 등장하게 된다.
  • 정밀한 URL패턴을 적용할 수 있다.

인터셉터(Interceptor)의 메소드 종류

HandlerInterceptor 인터페이스 사용

preHandle() :

  • Controller가 호출되기 전에 실행된다.
  • 컨트롤러 이전에 처리해야 하는 전처리 작업이나 요청 정보를 가공하거나 추가하는 경우에 사용할 수 있다.

postHandle() :

  • Controller가 호출된 후에 실행된다. ( View 렌더링 전)
  • 컨트롤러 이후에 처리해야 하는 후처리 작업이 있을 때 사용할 수 있다.

afterCompletion() :

  • 모든 뷰에서 최종 결과를 생성하는 일을 포함해 모든 작업이 완료된 후에 실행된다. ( View 렌더링 후)
    요청 처리 중에 사용한 리소스를 반환할 때 사용할 수 있다.

🧩로그 인터셉터

LogInterceptor

public class LogInterceptor implements HandlerInterceptor{
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		String requestURI = request.getRequestURI();
		System.out.println("[interceptor requestURI : " + requestURI);
		
		return true; // false -> 진행 X
	}
	
	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		System.out.println("[interceptor] postHandle");
	}
	
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
		System.out.println("[interceptor] afterCompletion");
	}
	
}

WebConfig

  • 생성한 스프링 인터셉터(LogInterceptor)를 설정에 등록

  • WebMvcConfigurer 인터페이스를 구현하여 addInterceptor 메서드를 재정의해서 인터셉터 등록이 가능하다.

  • addInterceptor : 인터셉터를 등록한다.

  • order(1) : 인터셉터의 호출 순서를 지정하며 낮을 수록 먼저 호출된다.

  • addPathPatterns("/") : 인터셉터를 적용할 URL 패턴을 지정한다.

  • excludePathPatterns("/css/", "/*.ico", "/error") : 인터셉터에서 제외할 패턴을 지정한다.

  • addPathPatterns("/sub1/test1", "/sub1/test2")

    • 1개의 "어떠한" 경로에 상관없이 쓰려면 /* -> /sub1/*
    • 1개를 넘어서서 몇개의 어디든지의 경로에 추가하고 싶으면 -> sub1/**
@Component
public class WebConfig implements WebMvcConfigurer{
	
	@Override
		public void addInterceptors(InterceptorRegistry registry) {
			registry.addInterceptor(new LogInterceptor())
				.order(1)
				.addPathPatterns("/**")
				.excludePathPatterns("/error");
			
			registry.addInterceptor(new LoginCheckInterceptor())
				.order(2)
				.addPathPatterns("/**") 	// 모든 경로 전체
				.excludePathPatterns("/", "/members/add", "/login", "/logout","/css/**");	
	}

🧩 로그인 인증 체크 인터셉터

LoginCheckInterceptor

public class LoginCheckInterceptor implements HandlerInterceptor{

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		String requestURI = request.getRequestURI();
		System.out.println("[interceptor] : " + requestURI);
		HttpSession session = request.getSession(false);
		
		if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER)== null) {
			System.out.println("[미인증 사용자 요청]");
			// 로그인으로 redirect
			response.sendRedirect("/login?redirectURL = "+ requestURI);
			return false;
		}
		return true;
	}
}


정리

  • 필터와 인터셉터 모두 비즈니스 로직과 분리되어 특정 요구사항(보안, 인증, 인코딩 등)을 만족시켜야 할 때 적용한다.

  • 필터(Filter)는 특정 요청과 컨트롤러에 관계없이 전역적으로 처리해야 하는 작업이나

  • 웹 어플리케이션에 전반적으로 사용되는 기능을 구현할 때 적용하고,

  • 인터셉터(Interceptor)는 클라이언트의 요청과 관련된 작업에 대해 추가적인 요구사항을 만족해야 할 때 적용한다.

출처

https://dev-coco.tistory.com/173#--%--%ED%--%--%ED%--%B--Filter-
https://catsbi.oopy.io/9ed2ec2b-b8f3-43f7-99fa-32f69f059171

0개의 댓글