스프링 부트 - 로그인2: 필터, 인터셉터

SeungTaek·2021년 8월 16일
1
post-thumbnail
post-custom-banner

본 게시물은 스스로의 공부를 위한 글입니다.
틀린 내용이 있을 수 있습니다.

로그인 1편 보러가기: 쿠키, 세션

  • 웹 페이지가 로그인된 사용자에게만 들어갈 수 있다고 해보자.

  • 그럼 모든 컨트롤러에 로그인 여부를 확인하는 코드를 짜야하는데... 코드 중복도 많이진 뿐더러, 로그인 로직이 바뀌게되면 작성한 모든 로직을 수정해야 한다.

  • 이렇게 애플리케이션 여러 로직에서 공통으로 관심이 있는 있는 것을 공통 관심사(cross-cutting concern)라고 한다.

  • 이러한 공통 관심사는 스프링의 AOP로도 해결할 수 있지만, 웹과 관련된 공통 관심사는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다

📒서블릿 필터

필터 흐름

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

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

  • 필터에서 로직을 통해 서블릿을 호출하지 않을 수 있다.

  • 필터 구현 로직 중 chain.doFilter(request, response)를 넣으면 필터를 체인으로 수행할 수 있다. 호출할 추가 필터가 없으면 서블릿이 뜬다. 반드시 넣어야 한다. 안넣으면 다음 진행이 안된다.

📌필터 구현

  • 필터를 구현할 땐 인터페이스인 Filter을 이용한다.
  • 오버라이드 하는 주요 메서드는 다음과 같다.
    • public default void init: 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.
    • public void doFilter:고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.
    • public default void destroy: 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.
@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 {
 			if (isLoginCheckPath(requestURI)) {
 				HttpSession session = httpRequest.getSession(false);
		 		if (session == null || session.getAttribute("login") == 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);
	 }
}
  • whitelist로 인증 체크를 하고싶지 않은 url을 등록한다.

📌필터 등록

@Configuration
public class WebConfig {
	@Bean
 	public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
	 	filterRegistrationBean.setFilter(new LogFilter()); //내가 구현한 필터 넣기
 	 	filterRegistrationBean.setOrder(1); //필터 체인할 때 가장 먼저 실행
 		 filterRegistrationBean.addUrlPatterns("/*"); //모든 요청 url에 대해 실행
 	 	return filterRegistrationBean;
 	}
}

📒스프링 인터셉터

  • 서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링 MVC가 제공하는 기술이다.

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

스프링 인터셉터 제한

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출 X) // 비 로그인 사용자

스프링 인터셉터 체인

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

📌스프링 인터셉터 인터페이스 구현하기

  • 스프링의 인터셉터를 사용하려면 HandlerInterceptor 인터페이스를 구현하면 된다.
  • default boolean preHandle: 컨트롤러 호출 전 호출
    • return이 true면 다음 진행, false면 더 이상 진행하지 않는다.
  • default void postHandle: 컨트롤러 호출 후에 호출
    • 컨트롤러에서 예외가 발생하면 호출 안함
  • default void afterCompletion: 요청 완료 이후
    • 뷰 렌더링 이후에 호출, 예외 발생시 Exception 파라미터로 예외를 받을 수 있음(정상 흐름에선 null 파라미터)
    • 예외와 무관하게 공통 처리를 하려면 이 메소드를 구현해야 한다.
  • 로그인 기능 인터셉터를 구현해보자.(preHandle만 사용할거다.)
  public class LoginCheckInterceptor implements HandlerInterceptor {
   	@Override
   	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
   		String requestURI = request.getRequestURI();
   		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 LoginCheckInterceptor()) //인터셉터 등록. 여기서 LoginCheckInterceptor()은 내가 구현한 클래스 이름이다.
 				.order(1) //낮을 수록 먼저 호출
 				.addPathPatterns("/**") //인터셉터를 적용할 url 패턴
 				.excludePathPatterns("/css/**", "/*.ico", "/error"); //인터셉터에서 제외할 패턴 지정
    }
}

📒더 나아가서..

📌필터 vs 인터셉터

  • 인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공한다.
  • 스프링 MVC를 사용하고, 특별히 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 것이 더 편리하다.

📌ArgumentResolver 활용

  • 구현을 하다보면 컨트롤러에서 현재 로그인된 회원의 정보를 받아야 하는 경우가 있다.
  • 그럼 아래와 같은 코드를 사용하면 된다.
@GetMapping("/")
public String login(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {
        if (loginMember == null) { //세션에 없는 사용자.
            return "login";
        }

        //로그인된 회원
        model.addAttribute("member", loginMember);
     	return "home";
 }

음.. 좀 길지 않나? 이걸 사용할 컨트롤러마다 다 쓸 생각하니 머리가 어질😵

  • ArgumentResolver을 사용하자! 그럼 아래와 같은 코드로 받을 수 있다.
@GetMapping("/")
public String login(@Login Member loginMember, Model model) {
        if (loginMember == null) { //세션에 없는 사용자.
            return "login";
        }

        //로그인된 회원
        model.addAttribute("member", loginMember);
     	return "home";
 }
  • @Login 애노테이션이 있으면 직접 만든 ArgumentResolver 가 동작해서 자동으로 세션에 있는 로그인 회원을 찾아주고, 만약 세션에 없다면 null 을 반환하도록 개발해보자.

  1. @Login 애노테이션 생성
@Target(ElementType.PARAMETER) //파라미터에만 사용할겁니다.
@Retention(RetentionPolicy.RUNTIME) //런타임까지 애노테이션 정보가 남아있게 하기위해.
public @interface Login {
}
  1. HandlerMethodArgumentResolver 구현
@Slf4j
public class LoginMemberArgumentResolver implementsHandlerMethodArgumentResolver {
    @Override
    //resolveArgument를 실행하기 위한 조건
    //@Login 애노테이션이 존재 && Member 타임이여야 한다.
	public boolean supportsParameter(MethodParameter parameter) {
 		boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
 		boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
 		return hasLoginAnnotation && hasMemberType;
 	}
    
	@Override
    //세션에 있는 member 객체를 찾아서 반환. 못찾으면 null 반환
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
		HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
 		HttpSession session = request.getSession(false);
 		if (session == null) {
			return null;
 		}
 		return session.getAttribute(SessionConst.LOGIN_MEMBER);
 	}
}
  1. 등록하기
@Configuration
public class WebConfig implements WebMvcConfigurer {
	@Override
 	public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
 		resolvers.add(new LoginMemberArgumentResolver());
 	}
}

로그인 1편 보러가기: 쿠키, 세션


인프런의 '스프링 MVC 2편(김영한)'을 스스로 정리한 글입니다.
자세한 내용은 해당 강의를 참고해주세요

profile
I Think So!
post-custom-banner

0개의 댓글