[ Spring ]/SpringMVC 2편

[Spring] 로그인 처리2 - 필터, 인터셉터

쿠릉쿠릉 쾅쾅 2022. 3. 13. 08:05
728x90

 

 

공통 관심사

  • 애플리케이션 여러 로직에서 공통으로 관심있는 것을 공통 관심사(cross-cutting concern)라고 한다.
  • 공통 관심사는 스프링의 AOP로 해결할 수 있지만, 웹과 관련된 공통 관심사는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다.
    • 예) 등록, 수정, 삭제, 조회 등등 여러 로직에서 공통으로 인증해야 하는 것 (로그인 여부 체크 로직)
  • 웹과 관련된 공통 관심사를 처리할 때는 HTTP의 헤더나 URL의 정보들이 필요한데, 서블릿 필터나 스프링 인터셉터는 HttpServletRequest를 제공한다.

 

 


 

 

서블릿 필터

  • 필터는 서블릿이 지원하는 수문장이다.
  • 만약에 모든 고객의 요청 로그를 남기는 요구사항이 있다면 필터를 사용하면 된다.
  • 필터는 특정 URL 패턴에 적용할 수 있다. /* 라고 하면 모든 요청에 필터가 적용된다.
  • 스프링을 사용하는 경우 서블릿은 스프링의 디스패처 서블릿을 의미한다.
  • 필터를 등록할 때 FilterRegistrationBean을 사용하자. @SerletComponentScan, @WebFilter(filterNmae="", urlPattersn="") 등으로 필터 등록이 가능하지만 필터 순서를 지정할 수 없다.

 

🔍 필터 흐름

  • HTTP 요청 → WAS → 필터 → 서블릿 → 컨트롤러
  • 필터를 적용하면 필터가 호출 된 다음에 서블릿이 호출된다.

 

🔍 필터 제한

  • HTTP 요청 → WAS → 필터 → 서블릿 → 컨트롤러
    • 예) 로그인 사용자
  • HTTP 요청 → WAS → 필터 (적절하지 않은 요청이라 판단, 서블릿 호출x)
    • 예) 비 로그인 사용자
  • 필터에서 적절하지 않은 요청이라고 판단하면 거기서 끝을 낸다. 그래서 로그인 여부를 체크하기에 좋다.

 

🔍 필터 체인

  • HTTP 요청 → WAS → 필터1 → 필터2 → 필터3 → 서블릿 → 컨트롤러
    • 필터는 체인으로 구성될 수 있다.
      • 필터를 여러개 추가 하는 형식으로 필터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()  : 고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.
  •  destroy()  : 필터 종료 메서드. 서블릿 컨테이너가 종료될 때 호출된다.
  •  doFilter()  와  destroy()  메서드는 default 메서드이기에 필수로 구현하지 않아도 된다.

 

 

🔍 서블릿 필터 - 요청 로그

  • 가장 단순한 필터인 모든 요청을 로그로 남기는 필터 만들기

 

📌 LogFilter (로그 필터)

package hello.login.web.filter;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;

@Slf4j
public class LogFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("log filter init");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("log filter doFilter");

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();
        String uuid = UUID.randomUUID().toString();

        try {
            log.info("REQUEST [{}] [{}]", uuid, requestURI);
            chain.doFilter(request, response);
        } catch (Exception e) {
            throw e;
        } finally {
            log.info("RESPONSE [{}] [{}]", uuid, requestURI);
        }

    }

    @Override
    public void destroy() {
        log.info("log filter destroy");
    }
}

 

  •  public class LogFilter implements Filter {} 
    • 필터를 사용하려면 필터 인터페이스를 구현해야 한다.
  •  doFilter(ServletRequest request, ServletResponse response, FilterChain chain)  
    • HTTP 요청이 오면  doFilter()  메서드가 호출된다.
    • ServletRequest request는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스다.
    • HTTP를 사용하면  HttpServletRequest httpRequest = (HttpServletRequest) request;  처럼 다운 케스팅을 해야한다.
  •  String uuid = UUID.randomUUID().toString(); 
    • HTTP 요청을 구분하기 위해 요청당 임의의 uuid를 생성한다.
  •  log.info("REQUEST [{}][{}]", uuid, requestURI); 
    • uuidrequestURI를 출력한다.
  •  chain.doFilter(request, response); 
    • 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다.
    • 만약 이 로직이 없으면 다음 단계로 진행되지 않는다.

 

📌  WebConfig (필터 설정)

package hello.login;

import hello.login.web.filter.LogFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();

        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");

        return filterRegistrationBean;
    }
}
  • 필터를 등록하는 방법은 여러가지가 있지만, 스프링 부트를 사용한다면 FilterRegistrationBean을 사용해서 등록한다.
  •  filterRegistrationBean.setFilter(Filter filter)  
    • 등록할 필터를 지정한다.
  •  filterRegistrationBean.setOrder(int order)  
    • 필터의 우선순위를 정한다.
    • 필터는 체인으로 동작한다. 따라서 순서가 필요하다.
    • int order값이 낮을수록 우선순위가 높다.
  •  addUrlPatterns(String... urlPatterns)  
    • 필터를 적용할 URL 패턴을 지정한다.
    • 한 번에 여러 패턴을 지정할 수 있다.
    • 모르면 URL 패턴을 검색할 것
    • URL 패턴을 /* 으로 등록하면 모든 URL을 의미한다. (모든 요청에 해당 필터가 적용된다.)

 

 

🔍 서블릿 필터 - 인증 체크

  • 로그인이 되지 않은 사용자는 상품 관리 페이지에 못 들어가도록 해야 한다.

 

📌 LoginCheckFilter (로그인 체크 필터)

package hello.login.web.filter;


import hello.login.web.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.PatternMatchUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

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

}
  •  private static final String[] whiteList = {"/", "/members/add", "/login", "/logout", "/css/*"}; 
    • whiteList 배열을 선언하여 인증 필터가 적용되지 않는 url을 적어놓는다.
    • 로그인이 필요없어도 접속할 수 있는 곳을 의미한다.
  •  isLoginCheckPath(requestURI)  
    • 화이트 리스트를 제외한 모든 경우에 인증 체크 로직을 적용한다.
    • whiteList에 없는 url 경로일 경우 true 값이 나오도록 설정한다.
  •  httpResponse.sendRedirect("/login?redirectURL=" + requestURI); 
    • 미인증 사용자는 로그인 화면으로 리다이렉트 한다.
    • 로그인 완료 후 다시 원래 보던 페이지로 이동시키기 위해 원래 보던 페이지의 URI 주소를 로그인 페이지로 보낼 때  쿼리 파라미터로 전달한다.
    • /login 컨트롤러에서 로그인 성공시 해당 경로로 이동하는 기능을 추가로 개발해야한다.

 

📌 WebConfig (필터 설정)

package hello.login;

import hello.login.web.filter.LogFilter;
import hello.login.web.filter.LoginCheckFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();

        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");

        return filterRegistrationBean;
    }

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

        return filterRegistrationBean;
    }
}
  • 로그인체크 필터는 /*으로 URL을 모두 허용해준 상태이다. 필터 내부에서 화이트리스트가 있기 때문에 따로 불필요한 검사를 해주지 않는다. 

 

📌 컨트롤러 (로그인)

@PostMapping("/login")
public String loginV4(@Validated @ModelAttribute LoginForm form,
                      BindingResult bindingResult,
                      @RequestParam(defaultValue = "/") String redirectURL,
                      HttpServletRequest request) {

    if (bindingResult.hasErrors()) {
        return "login/loginForm";
    }

    Member loginMember = loginService.login(form.getLoginId(), form.getPassword());

    if (loginMember == null) {
        bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
        return "login/loginForm";
    }

    // 로그인 성공 처리
    // 세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
    HttpSession session = request.getSession();

    // 세션에 로그인 회원 정보 보관
    session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

    return "redirect:" + redirectURL;
}
  • /login 컨트롤러에서 redirectURL 요청 파라미터를 추가로 요청했다. 이 값을 사용해서 로그인 성공시 해당 경로로 고객을 redirect 한다.
    • 로그인이 성공했을 경우 redirectURL 이라는  @RequestParam  을 조회해 만약 다은 페이지로 접근을 시도하다가 로그인 페이지로 온 경우 다시 되돌아가기 위해 사용한다.

 

 

🧷 참고

  • 필터는 스프링 인터셉터에서 제공하지 않는 기능을 가지고 있다.
    • 필터는  chain.doFilter(request, response)  메서드를 통해서 다음 필터 또는 서블릿을 호출한다. 이 때 파라미터인 requestresponse를 다른 객체로 바꿔서 전달할 수 있다.
    • 다른 객체를 전달하려면 ServletRequestServletResponse를 구현한 객체이어야 한다.
    • 많이 쓰이는 기능은 아니지만 알고는 있자.

 

 


 

 

스프링 인터셉터

  • 웹과 관련된 공통 관심사를 처리하기 위해 서블릿에는 필터가 있다면 스프링에는 인터셉터가 있다. 
  • 서블릿 필터는 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링 MVC가 제공하는 기술이다.
  • 둘 다 웹과 관련된 공통 관심사를 처리하지만, 적용 되는 순서, 범위 그리고 사용방법이 다르다.
  • 스프링 인터셉터는 URL 패턴을 적용할 수 있다. 서블릿 필터의 URL 패턴과 달리 매우 정밀하게 설정 할 수 있다.
  • 스프링 인터셉터는 서블릿 필터보다 편리하고 더 정교하고 다양한 기능을 지원한다.
  • 서블릿 필터보다 인터셉터를 사용하는 것이 더 좋다.

 

🔍 스프링 인터셉터 흐름

  • HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러 
  • 스프링 인터셉터는 디스패치 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출 된다.

 

🔍 스프링 인터셉터 제한

  •  HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러
    • 예) 로그인 사용자
  • HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 (적절하지 않은 요청이라 판단, 컨트롤러 호출X)
    • 예) 비 로그인 사용자
  • 인터셉테에서 적절하지 않은 요청이라고 판단하면 컨트롤러를 호출하지 않고 거기서 끝낸다.
  • 로그인 여부를 체크하기에 좋다.

 

🔍 스프링 인터셉터 체인

  • HTTP 요청 → WAS → 필터  → 서블릿  → 인터셉터1 → 인터셉터2 → 인터셉터3 → 컨트롤러
  • 스프링 인터셉터는 체인으로 구성되어 있다. 중간에 인터셉터를 자유롭게 추가할 수 있다.

 

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

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 {}
    
}
  • 스프링 인터셉터를 사용하려면 HandlerInterceptor 인터페이스를 구현하면 된다.
  • 인터셉터는 컨트롤러 호출전(preHandle), 호출 후(postHandle), 요청 완료 이후(afterCompletion)와 같이 단계적으로 세분화되어있다.
  • 서블릿 필터는 단순히 request, response만 제공했지만, 인터셉터는 어떤 컨트롤러(handler)가 호출되는지 호출 정보도 받을 수 있다.
  • 스프링 인터셉터는 어떤 modelAndView가 반환되는지 응답 정보를 받을 수 있다.

 

🔍 스프링 인터셉처 호출 흐름

💡 정상 흐름

  • preHandle
    • 컨트롤러가 호출되기 전에 호출된다. (정확히는 핸들러 어댑터가 호출되기 전에 호출된다.)
    • preHandle의 반환타입은 boolean이다. 반환 값이 true 일 경우 다음 단계로 진행된다. false 일 경우 더 이상 진행되지 않는다.
  • postHandle
    • 컨트롤러 호출 후에 호출된다. (정확히는 핸들러 어댑터가 호출된 후에 호출된다.)
  • afterCompletion
    • 뷰가 렌더링 된 후에 호출된다.
    • 정상 작동시 예외 파라미터에는 null 값이 들어온다.

 

💡 예외 상황

  • preHandle
    • 컨트롤러가 호출되기 전에 호출된다.
  • postHandle
    • 컨트롤러에서 예외가 발생하면 postHandle은 호출되지 않는다.
  • afterCompletion
    • afterCompletion은 항상 호출된다. (예외가 발생해도 호출된다.)
    • 예외를 파라미터로 받을 수 있어서 예외 발생시 어떤 예외가 발생했는지 로그로 출력할 수 있다.
    • 예외와 무관하게 공통 처리를 하려면  afterCompletion()  메서드에 로직을 작성해야 한다.
  • 스프링 인터셉터 혹은 컨트롤러에서 예외가 발생하면 각 시점에 따라 호출 여부가 달라진다.

 

 

🔍 스프링 인터셉터 - 요청 로그

 

📌 LogInterceptor (요청 로그 인터셉터)

package hello.login.web.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@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("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 uuid = (String )request.getAttribute(LOG_ID);

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

        if (ex != null) {
            log.error("afterCompletion error", ex);
        }
    }
}
  •  String uuid = UUID.randomUUID().toString()  
    • 요청 로그를 구분하기 위해 uuid를 생성한다.
  •  request.setAttirbute(LOG_ID, uuid)   
    • 서블릿 필터의 경우 지역변수로 해결이 가능하지만, 스프링 인터셉터는 호출 시점이 완전히 분리되어 있다.
    • preHandle에서 지정한 값을 postHandle, afterCompletion에서 함께 사용하기 위해 request에 담아두었다.
    • 스프링 인터셉터는 WAS에 의해 싱글톤으로 관리된다. 그래서 멤버변수를 사용하면 안된다.
    • request에 담아둔 값은  request.getAttribute(LOG_ID) 로 찾는다.
  •  return true;  
    • true면 정송 호출이다. 다음 인터셉터나 컨트롤러가 호출된다.

 

📌 핸들러 정보  

// @RequestMapping : HandlerMethod
// 정적 리소스 : ResourceHttpRequestHandler
if (handler instanceof HandlerMethod) {
    HandlerMethod hm = (HandlerMethod) handler;  // 호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
}
  • ResourceHttpRequestHandler
    •  @Controller 가 아닌 /resources/static 와 같은 정적 리소스가 호출된 경우 스프링 인터셉터의 Object handler 매개변수에 ResourceHttpRequestHandler가 핸들러 정보로 넘오온다.

 

📌 WebConfig (인터셉터 등록)

package hello.login;

import hello.login.web.filter.LogFilter;
import hello.login.web.filter.LoginCheckFilter;
import hello.login.web.interceptor.LogInterceptor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.Filter;

@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(HandlerInterceptor interceptor)  
    •  인터셉터를 등록한다.
  •  order(int order)  
    • 인터셉터의 호출 순서를 지정한다. 낮을수록 우선순위가 높다.
  •  addPathPatterns(String... patterns)   
    • 인터셉터를 적용할 URL 패턴을 지정한다.
  •  excludePathPatterns(String... patterns) 
    • 인터셉터에서 제외할 URL 패턴을 지정한다.
  • 서블릿 필터와 비교해보면 인터셉터는 addPathPatterns, excludePathPatterns로 매우 정밀하게 URL 패턴을 지정할 수 있다.

 

🧷 스프링의 URL 경로 패턴

  • 스프링이 제공하는 URL 경로 패턴은 서블릿 기술이 제공하는 URL 경로 패턴과 다르다.
  • 스프링이 제공하는 URL 경로 패턴이 더 자세하고 세밀하게 설정할 수 있다.

 

PathPattern 공식 문서

?  ▶ 한 문자 일치
* ▶ 경로(/) 안에서 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이라는 변수로 캡처

/pages/t?st.html ▶ matches /pages/test.html, /pages/tXst.html but not /pages/ toast.html
/resources/*.png ▶ matches all .png files in the resources directory
/resources/** ▶ matches all files underneath the /resources/ path, including / resources/image.png and /resources/css/spring.css
/resources/{*path} ▶ matches all files underneath the /resources/ path and captures their relative path in a variable named "path"; /resources/image.png will match with "path" → "/image.png", and /resources/css/spring.css will match with "path" → "/css/spring.css"
/resources/{filename:\\w+}.dat will match /resources/spring.dat and assign the value "spring" to the filename variable

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html

 

PathPattern (Spring Framework 5.3.16 API)

Representation of a parsed path pattern. Includes a chain of path elements for fast matching and accumulates computed state for quick comparison of patterns. PathPattern matches URL paths using the following rules: ? matches one character * matches zero or

docs.spring.io

 

 

🔍 스프링 인터셉터 - 인증 체크

 

📌 LoginCheckInterceptor (로그인 인터셉터)

package hello.login.web.interceptor;

import hello.login.web.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@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();

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

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

        return true;
    }
}
  • 인증이라는 것은 컨트롤러 호출 전에만 호출하면 된다. 따라서 preHandle만 구현하면 된다.
  • 서블릿 필터와 비교해서 코드가 매우 간결하다.

 

📌 WebConfig (인터셉터 등록)

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

}

 

 

🧷 정리

  • 서블릿 필터와 스프링 인터셉터는 웹과 관련된 공통 관심사를 해결하기 위한 기술이다.
  • 서블릿 필터와 비교해서 스프링 인터셉터가 개발자 입장에서 훨씬 편리하다.

 

 


 

 

ArgumentResovler 활용

  • 클라이언트로 받은 Request 정보에서 Session 정보를 꺼내 해당하는 세션키로 로그인 정보를 찾는 방법을 다양하게 사용할 수 있다.

 

🔍 기존 방식

💡 HttpServletRequest에서 직접 꺼내기

@GetMapping("/")
public String homeLoginV3(HttpServletRequest request, Model model) {

    HttpSession session = request.getSession(false);

    if (session == null) {
        return "home";
    }

    Member loginMember = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);

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

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

 

💡 @SessionAttirbute 애노테이션 활용하기

@GetMapping("/")
public String homeLoginV4(
        @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false)Member loginMember, Model model) {

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

    // 세션이 유지되면 로그인으로 이동
    model.addAttribute("member", loginMember);
    return "loginHome";
}
  • 애노테이션 기반의 매핑방식이 직접 로직을 구현하는 것보다 간결하다.
  • 하지만 매번 속성(name, required)을 작성해주는 것이 번거롭고 해당 애노테이션을 통해 해당 객체에 대한 명시성이 부족하다.

 

 

🔍 ArgumentResolver 사용하기

 

📌 @Login

package hello.login.web.argumentresolver;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login { }
  •  @Target(ElementType.PARAMETER) 
    • 파라미터만 붙일 수 있는 애노테이션이다.
  •  @Retention(RetentionPolicy.RUNTIME) 
    • 리플렉션 등을 활용할 수 있도록 런타임까지 애노테이션 정보가 남아있도록 한다.

 

📌 LoginMemberArgumentResolver

package hello.login.web.argumentresolver;

		import hello.login.domain.member.Member;
		import hello.login.web.SessionConst;
		import lombok.extern.slf4j.Slf4j;
		import org.springframework.core.MethodParameter;
		import org.springframework.web.bind.support.WebDataBinderFactory;
		import org.springframework.web.context.request.NativeWebRequest;
		import org.springframework.web.method.support.HandlerMethodArgumentResolver;
		import org.springframework.web.method.support.ModelAndViewContainer;

		import javax.servlet.http.HttpServletRequest;
		import javax.servlet.http.HttpSession;

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

	@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);
	}
}
  • ArgumentResolverHandlerMethodArgumentResolver 인터페이스를 구현해야 한다.
  •  supportsParameter() 
    • 컨트롤러 호출시 매개변수들은 ArgumentResolver에 의해 매핑이 되는데, 많은 ArgumentResolver가 각각 대응할 수 있는 객체는 제한되어 있는데, 이를 책임 사슬 패턴을 이용해 처리한다.
    • 각각 ArgumentResolver는  supportsParameter()  메서드를 이용해 매핑 가능 여부를 boolean 타입으로 반환한다.
    • @Login 애노테이션이 붙어있으면서 Member 객체인 경우 ArgumentResolver가 사용된다.
  •  resolverArgument()  
    • 컨트롤러가 호출되기 전에 해당 메서드가 호출되어 필요한 파라미터 정보를 생성한다.
      • 컨트롤러에 필요한 파라미터 정보를 생성한다.
    • 여기서는 세션에서 로그인 회원 정보인 member객체를 찾아 반환한다.
    • 이후 스프링 MVC에서 컨트롤러의 메서드를 호출하면서 여기에서 반환된 member 객체를 파라미터로 전달한다.

 

📌 HomeController (홈 컨트롤러)

@GetMapping("/")
public String homeLoginV4ArgumentResolver(@Login Member loginMember, Model model) {
    String result = "loginHome";

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

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

 

📌 WebConfig (설정 추가)

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new LoginMemberArgumentResolver());
    }
}
  • 서블릿 필터나 스프링 인터셉터처럼 WebMvcConfigurer에 리졸버를 등록해야 한다.

 

 

🧷 정리

  • 애노테이션을 활용하면 더 명시적이고 간결하게 회원정보를 찾아서 매핑해줄 수 있다.
  • 만약 회원 객체(Member)의 구조가 바뀐다면 Resolver 쪽만 수정하면 된다.
  • 직접 객체 매핑 로직을 구현하면 코드가 너무 길고 다른 개발자가 보기 쉽지 않다.
  • @SessionAttribute 애노테이션을 활용하면 좀 나아지지만 목적 자체가 로그인 정보를 찾는다는 특정 목적의 애노테이션이 아니기 때문에 다른 개발자가 보기 힘들다.
  • 하지만 ArgumentResolver를 이용해 애노테이션으로 요청 매핑 핸들러 어댑터를 구현해주면 하나의 특정 애노테이션으로 가독성도 좋고 편리하게 회원 정보를 조회할 수 있다.
  • ArgumentResolver를 활용하면 공통작업이 필요할 때 컨트롤러를 더욱 편리하게 사용할 수 있다.

 

 

 

 

 

 

 


👀 참고 자료

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

 

https://catsbi.oopy.io/9ed2ec2b-b8f3-43f7-99fa-32f69f059171

 

7. 로그인 처리2 - 필터, 인터셉터

목차

catsbi.oopy.io

 

 

728x90