둘셋 개발!

[spring mvc 2편-웹 개발 활용 기술 - 5 (2) ] 로그인 처리 - 필터,인터셉터 본문

SPRING/MVC

[spring mvc 2편-웹 개발 활용 기술 - 5 (2) ] 로그인 처리 - 필터,인터셉터

23 2022. 2. 8. 02:35

공통 관심 사항 처리

로그인을 한 사용자만 상품 관리 페이지에 접근할 수 있게 했으면 좋겠다!

 

라는 요구사항이 있다면 상품을 등록, 수정, 삭제, 조회를 하는 모든 컨트롤러에 로그인 여부를 체크하는 로직을 넣으면 된다.

하지만 같은 로직을 여러번 넣는다는 반복문제도 있고 로그인 로직이 변경되면 번거로워 진다.

 

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

공통 관심사는 서블릿이 지원하는 필터와 스프링이 지원하는 인터셉터로 처리할 수 있다.

 


서블릿 필터 개념

1. 필터의 흐름

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

 

2. 필터 제한

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

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

 

3. 필터 체인

- 필터는 체인으로 구성

- WAS이후 필터가 호출되면 여러개의 필터가 순서대로 호출되고 모든 필터가 호출된 뒤에 서블릿이 호출

 

4. 필터 인터페이스

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

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() : 필터를 종료하는 메서드로 서블릿 컨테이너가 종료될 때 호출

 

 


서블릿 필터 적용

1. 인증 체크 필터 구현( 로그인 인증 체크 )

@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);
                    //로그인으로 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

로그인 인증필터를 사용하지 않는 경로를 보관한다.

 

-isLoginCheckPath

요청경로를 받아서 whitelist에 포함되면 False, 포함이 되지 않으면 즉 인증을 거쳐야하는 경로면 true를 반환한다.

 

-httpResponse.sendRedirect

미인증 사용자가 요청했으면 로그인 화면으로 리다이렉트 해준다.

requestURI가 포함된 이유는, 로그인 화면으로 리다이렉트하고 로그인을 한 이후에 사용자가 원했던(필터를 통과하지 못했던) 경로로 바로 가기 위해서이다.

 

로그인 컨트롤러에서 다음과 같은 처리를 해야한다.

@PostMapping("/login")
    public String loginV4(@Valid @ModelAttribute LoginForm loginForm,
                          BindingResult bindingResult,
                          @RequestParam(defaultValue = "/") String redirectURL,
                          HttpServletRequest request) {
                          
        //실패로직..

        //성공로직..

       
        return "redirect:" + redirectURL;
    }

@RequestParam을 사용해서 리다이렉트 할 URL(sendRedirect로 보내진 requestURI)를 받고 

로그인에 성공하면 다시 리다이렉트 해준다.

 

-return

httpResponse.sendRedirect 뒤에 return; 을 꼭 넣어주어야 한다.

넣어주면 필터는 더이상 진행하지 않고 당연히 서블릿과 컨트롤러도 호출되지 않는다.

앞에 redirect를 해줬기 때문에 redirect가 응답으로 적용되고 요청은 끝난다.

 

 

여기서 끝이 아니라 LoginCheckFilter를 어딘가에 등록시켜 작동하게 해야한다.

클래스를 새로 생성하여 다음과 같이 적용해준다.

2. 인증필터 등록

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

우선 filterRegistrationBean을 생성

 

-setFilter

여기에 필터를 등록한다

 

-setOrder

로그인체크필터를 몇 번째로 진행할지 정한다(필터체인을 순서대로 호출한 뒤 서블릿 호출)

 

-addUrlPatterns

어떤 경로에 필터를 적용할 지 진행한다.

"/*" 라고 했으니 모든 요청에 필터를 적용한다는 것을 알 수 있다.

 

 

이후에는 필터와 비슷한 역할을 하지만 조금 다른 인터셉터를 설명하겠다


스프링 인터셉터 개념

1. 스프링 인터셉터 흐름

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

 

2. 스프링 인터셉터 제한

1) 로그인 사용자
HTTP 요청   ->   WAS   ->  필터   ->   서블릿   ->   스프링 인터셉터   ->  컨트롤러

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

 

3. 스프링 인터셉터 체인

필터와 마찬가지로 여러개의 인터셉터를 체인으로 구성할 수 있다.

 

 

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

public interface HandlerInterceptor {

	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		return true;
	}

	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

컨트롤러 호출 후 수행 , 예외가 발생하면 더이상 호출x

modelAndView를 알 수 있음

 

-afterCompletion

컨트롤러 요청 완료 이후 수행, 예외가 발생하러라도 호출할 수 있고 예외를 받을 수 있음

 

 

 

그림으로 정리하면

 


스프링 인터셉터 적용

1. 인증 체크 인터셉터 구현

@Slf4j
public class LoginInterceptor 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();

        if(session ==null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null){
            log.info("미인증 사용자 요청");

            //로그인으로 redirect
            response.sendRedirect("/login?redirectURL="+requestURI);
            return false;
        }
        return true;
    }
}

필터와는 다르게 preHandle만 구현하면 된다. 왜냐하면 인증은 컨트롤러 전에만 호출되면 되기 때문이다.

인증에 실패하면 false를 리턴해주고 성공하면 true를 리턴해준다.

 

2. 인증 체크 인터셉트 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

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

WebMvcConfigurer가 제공하는 addInterceptors를 사용해서 인터셉터를 등록할 수 잇다.

 

-registry.addInterceptor

인증 체크 인터셉터를 넣는다.

 

-order

인터셉터를 적용할 순서다.

 

-addPathPatterns

인터셉터를 적용할 URL를 넣는다.

 

-excludePathPatterns

인터셉터에서 제외할 패턴을 지정한다.

필터는 whitelist를 따로 만들어서 필터를 적용 여부를 직접 코드로 작성해야 하는데 인터셉터에서는 편리하게 해결할 수 있다.

 

또한 필터보다 인터셉터에서 정밀하게 URL패턴을 지정할 수 있는 장점이 있다.

 

 

(참고 : 인프런 김영한 강사님 - 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 )