앞서 코드에서 세션을 이용해 로그인 기능을 구현하였다.
하지만 url을 통해 로그인을 하지 않는 사용자도 상품 관리 화면에 접근할 수 있는 문제가 있다.
각 컨트롤러에서 로그인 여부를 체크할 수도 있겠지만, 이 경우 로그인과 관련된 로직이 변경될 때 유지 보수성이 매우 어려워진다.
따라서 로그인 인증에 관한 '공통 관심사(cross-cutting concern)'를 설정하여 한 번에 처리하는 방법이 좋다.
→ AOP, 서블릿 필터, 스프링 인터셉터
1. 서블릿 필터
필터 흐름.
HTTP 요청 → WAS → 필터 → 서블릿(Dispatcher) → 컨트롤러
필터 체인으로 구성되며, 중간에 필터를 자유롭게 추가할 수 있다. ( LogFilter → LoginCheckFilter )
필터에 적절하지 않은 요청이 오면, 다음 과정인 서블릿과 컨트롤러를 호출하지 않고 해당 요청을 종료시켜야 한다! (return)
cf. 스프링 인터셉터는 제공하지 않는 기능 (잘 사용되지는 않음..)
chain.doFilter(request, response);
에서 다음 필터 또는 서블릿을 호출할 때, request / response 를 바꿔 넣을 수 있다.
[Filter]
public interface Filter {
// 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.
default void init(FilterConfig filterConfig) throws ServletException {
}
// HTTP 요청이 올 때마다 해당 메서드가 호출된다.
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
// 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.
default void destroy() {
}
}
[LogFilter]
@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); // 다음 필터 or 서블릿을 호출해주어야 한다.
} catch (Exception e) {
throw e;
} finally {
log.info("RESPONSE [{}][{}]", uuid, requestURI);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
[LoginCheckFilter]
@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, 로그인 이후 접속을 시도한 view로 바로 띄워주기 위해 requestURI를 포함시켜준다.
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);
}
}
[WebConfig]
// 필터를 등록하는 방법은 여러가지가 있지만, 스프링 부트를 사용한다면 FilterRegistrationBean 을 사용해서 등록하면 된다. (필터 순서 조절 가능)
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LogFilter()); // 필터 등록
filterFilterRegistrationBean.setOrder(1); // 필터는 체인으로 동작하므로 순서가 필요하다. (낮을수록 먼저 동작)
filterFilterRegistrationBean.addUrlPatterns("/*"); // 필터를 적용할 URL 패턴 (여러 패턴 지정 가능)
return filterFilterRegistrationBean;
}
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LoginCheckFilter()); // 필터 등록
filterFilterRegistrationBean.setOrder(2);
filterFilterRegistrationBean.addUrlPatterns("/*");
return filterFilterRegistrationBean;
}
}
2. 스프링 인터셉터
Spring MVC에서 '서블릿 필터'와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있게 제공하는 기술이다.
스프링 컨텍스트 내부에서 동작하면서, 컨트롤러 레벨에서 더 세밀한 제어가 가능하기 때문에 실무에서는 인터셉터를 더 많이 사용한다.
Filter | Interceptor | |
실행시점 | 1) 서블릿 컨테이너가 생성될 때 2) 컨트롤러 호출하기 전 3) 서블릿 컨테이너가 종료될 때(응답 전) |
1) 컨트롤러 호출하기 전 2) 컨트롤러 호출 후 3) 뷰가 렌더링 된 이후(응답 후) |
스프링 의존성 주입(DI) | X (스프링 컨텍스트 외부에서 동작함) | O |
URL 패턴 매핑 | PatternMatchUtils.simpleMatch 유틸리티 사용 | WebConfig에서 각 인터셉터마다 URL 패턴 매칭 가능 |
에러 처리 용이성 | 필터 수준에서의 예외 처리 (chain.doFilter 호출을 통해 다음 단계로 전달되는 과정에서 발생한 예외를 처리) |
컨트롤러 수준에서의 예외 처리 |
로깅 및 성능 모니터링 | 서블릿 레벨 | 컨트롤러 레벨 |
스프링 인터셉터 흐름.
HTTP 요청 → WAS → 필터 → 서블릿(Dispatcher) → 스프링 인터셉터 → 컨트롤러
인터셉터 체인에 적절하지 않은 요청에 대한 컨트롤러를 호출하지 않고 해당 요청을 종료하는 판단은 '서블릿 필터'와 동일하게 동작한다.
[HandlerInterceptor]
public interface HandlerInterceptor {
// 컨트롤러(핸들러 어댑터) 호출 전에 호출된다.
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
// 컨트롤러(핸들러 어댑터) 호출 후에 호출된다.
// 컨트롤러에서 예외가 발생하면 호출 X
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
// 뷰가 렌더링 된 이후에 호출된다.
// 예외(ex)를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있다.
// 항상 호출된다.
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
[LogInterceptor]
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
private static final String LOG_ID = "logId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String logId = UUID.randomUUID().toString();
// request 종료 시점까지 LOG_ID로 추적하기 위함.
// LogInterceptor가 싱글톤처럼 사용되기 때문에, 멤버변수로 사용하면 안된다.
request.setAttribute(LOG_ID, logId);
// RequestMapping : HandlerMethod
// 정적 리소스 : ResourceHttpRequestHandler
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler; // 호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
}
log.info("REQUEST [{}][{}][{}]", logId, requestURI, handler);
return true; // 다음 인터셉터 or 컨트롤러(핸들러) 호출
}
@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("RESPONSE [{}][{}][{}]", logId, requestURI, handler);
// 에러가 있을 로그 출력
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
[LoginCheckInterceptor]
@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("미인증 사용자 요청");
response.sendRedirect("/login?redirectURL=" + requestURI); // 로그인으로 redirect
return false;
}
return true;
}
}
[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");
}
}
'Spring > MVC 2' 카테고리의 다른 글
9. API 예외 처리 (0) | 2024.05.06 |
---|---|
8. 예외 처리와 오류 페이지 (0) | 2024.05.05 |
6. 로그인 처리1 - 쿠키, 세션 (1) | 2024.05.02 |
5. 검증 2 - Bean Validation (0) | 2024.05.01 |
4. 검증 1 - Validation (0) | 2024.04.29 |