스프링 mvc - 필터, 인터셉터

meluu_·2024년 1월 22일
0

스프링

목록 보기
23/27
post-thumbnail

🌿 시작하기 앞서


스프링 부트 3.2.1 버전을 기준으로 작성됨

로그인한 사용자만 특정 페이지에 입장 가능하게 설정해야한다.

하지만 모든 컨트롤러의 로직에 로그인 여부 체크를 넣는 것은 힘든 일이다.

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

공통 관심사는 스프링의 AOP로도 처리 가능하지만,
웹과 관련된 공통 관심사는 서블릿의 필터, 스프링의 인터셉터를 사용하는 것이 좋다.


🌱 서블릿 필터


필터의 흐름
HTTP -> WAS -> 필터 -> 서블릿 -> 컨트롤러
모든 고객의 요청 로그를 남기는 요구사항이 있다면 필터 사용

// 필터 제한
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 // 로그인 사용자

HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출 x) // 비 로그인 사용자

// 필터 체인
HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 서블릿 -> 컨트롤러 // 로그인 사용자

필터 인터페이스

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(): 고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.

    • chain.doFilter(request, response) 가장 중요한 부분인데 다음 필터 존재시 필터 호출, 없으면 서블릿 호출

    • doFilter를 호출하지 않으면 다음단계로 진행되지 않는다.

    • doFilter override후 안에서 호출하는 것임

  • destroy(): 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.

필터 인터페이스를 구현체로 사용한다.

필터 설정

// WebConfig 

@Configuration
public class WebConfig {
    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }
}
  • setFilter(new LogFilter()) : 등록할 필터를 지정한다.
  • setOrder(1) : 필터는 체인으로 동작한다. 따라서 순서가 필요하다. 낮을 수록 먼저 동작한다.
  • addUrlPatterns("/*") : 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다.

✔️ 인증 체크


@Slf4j
public class LogincheckFilter implements Filter {

    private static final String[] whilteList = {"/", "/members/add", "/login", "/logout", "/css/*"};

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    		
            
            // ...

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


    /**
     * 화이트 리스트이 경우 인증 체크X
     */
    private boolean isLoginCheckPath(String requestURL) {
        return !PatternMatchUtils.simpleMatch(whilteList, requestURL);
    }
}
  • 인증 필터를 적용해도 홈, 회원가입, 로그인 화면, css 같은 리소스에는 접근할 수 있어야 한다

  • sendRedirect 에서 requestURL을 쿼리 파라미터로 넣는 이유

    • 로그인 후 다시 접근하기 원했던 페이지로 가는 편의성을 위해서이다.
  • return 중요 , 필터 더 이상 진행 x , 서블릿, 컨트롤러도 더는 호출 x


webConfig 설정

// webConfig 에 loginCheckFilter 추가 

@Bean
public FilterRegistrationBean loginCheckFilter() {
    FilterRegistrationBean<Filter> filterRegistrationBean = new
            FilterRegistrationBean<>();
    filterRegistrationBean.setFilter(new LoginCheckFilter());
    filterRegistrationBean.setOrder(2);
    filterRegistrationBean.addUrlPatterns("/*");
    return filterRegistrationBean;
}

로그인 이후 redirect 처리


@PostMapping("/login")
public String loginV4(
        @Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
        @RequestParam(defaultValue = "/") String redirectURL, HttpServletRequest request) {
	
    // ...
    
    //redirectURL 적용
    return "redirect:" + redirectURL;
}

🌱 스프링 인터셉터


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

// 스프링 인터셉터 제한
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)
  • 호출 후 (postHandle)
  • 요청 완료 이후 (afterCompletion)
  • 세분화되어 있음
  • 어떤 컨트롤러가 호출되어있는지, 반환된 ModelAndView가 뭔지 정보도 받을 수 있음

afterCompletion 은 항상 호출
이 경우 예외( ex )를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력 가능

@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 HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler; // 호출한 컨트롤러 메서드의 모든 정보가 포함되어 있다.
            log.info("handler all info = {}", hm.toString());
        }

        log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
        return true;
    }

    @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();
        String logId = (String) request.getAttribute(LOG_ID);
        log.info("!!@REQUEST [{}][{}][{}]", logId, requestURI, handler);

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

WebConfig 에 인터셉터 등록

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

? 한 문자 일치
* 경로(/) 안에서 0개 이상의 문자 일치
** 경로 끝까지 0개 이상의 경로(/) 일치
{spring} 경로(/)와 일치하고 spring이라는 변수로 캡처
{spring:[a-z]+} matches the regexp [a-z]+ as a path variable named "spring"
{spring:[a-z]+} regexp [a-z]+ 와 일치하고, "spring" 경로 변수로 캡처
{*spring} 경로가 끝날 때 까지 0개 이상의 경로(/)와 일치하고 spring이라는 변수로 캡

✔️ 인증 체크


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

WebConfig 에 추가

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

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



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

WebMvcConfigurer에 설정 추가


@Configuration
public class WebConfig implements WebMvcConfigurer {

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

@Login 애노테이션

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

    log.info("resolverArgument 실행");

    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
가 사용
* resolveArgument() : 컨트롤러 호출 직전에 호출 되어서 필요한 파라미터 정보를 생성해준다. 여기서는 세션
에 있는 로그인 회원 정보인 member 객체를 찾아서 반환해준다. 이후 스프링MVC는 컨트롤러의 메서드를 호출
하면서 여기에서 반환된 member 객체를 파라미터에 전달해준다.


🔖 학습내용 출처

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술

profile
열심히 살자

0개의 댓글