김영한 개발자님의 스프링 MVC 2 강의를 수강하고 정리한 내용이다.
필터는 서블릿, 인터셉터는 스프링에서 제공하는 기능이다.
로그인하지 않은 사용자도 URL을 직접 호출하면 상품 관리 화면에 들어갈 수 있다.
웹과 관련된 공톰 관심사(애플리케이션 여러 로직에서 공통적으로 관심이 있는 것)에는 AOP 대신 필터 또는 인터셉터를 사용하는 것이 좋다.
필터는 서블릿이 지원하는 수문장이다.
HTTP요청 -> WAS(서버) -> 필터 -> 서블릿 -> 컨트롤러
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출X) //비 로그인 사용자
HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러
모든 요청을 로그로 남기는 필터를 개발한다.
@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();
// 모든 사용자의 요청 URI 남기기
String uuid = UUID.randomUUID().toString();
// 요청 온것을 구분하기 위해 UUID 사용
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");
}
}
필터를 쓸 수 있게 등록을 해야한다.
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter(){
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
// 필터의 순서 정해주기
filterRegistrationBean.addUrlPatterns("/*");
// 어떤 URL패턴에 적용하는가 (모든 URL에 적용)
return filterRegistrationBean;
}
}
스프링 부트를 사용한다면 FilterRegistrationBean
을 사용해서 등록하면 된다.
인증 받지 않으면 해당 페이지에 들어가지 못하게 한다.
@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);
}
}
앞서 필터를 쓰기위하여 등록했듯이, WebConfig
에 등록해준다.
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
위의 코드를 적용 후에 실행하면 리다이렉트가 제대로 되는 것을 확인할 수 있다.
@PostMapping("/login")
public String loginV4(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, @RequestParam(defaultValue = "/") String redirectURL, HttpServletRequest request) {
// @RequestParam(defaultValue = "/") String redirectURL,를 수정함
// 없으면 /로 갈꺼고 아니면 redirectURL로 가게 설정한다.
.....
return "redirect:" + redirectURL;
}
로그인 체크 필터에서, 미인증 사용자는 요청 경로를 포함해서 /login
에 redirectURL
요청 파라미터를 추가해서 요청했다. 이 값을 사용해서 로그인 성공시 해당 경로로 고객을 redirect
한다.
서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링 MVC가 제공하는 기술이다. 둘다 웹과 관련된 공통 관심 사항을 처리하지만, 적용되는 순서와 범위, 그리고 사용방법이 다르다.
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
서블릿 필터의 경우 단순하게 doFilter() 하나만 제공된다.
인터셉터는 컨트롤러 호출 전(preHandle), 호출 후(postHandle), 요청 완료 이후(afterCompletion)와 같이 단계적으로 잘 세분화 되어 있다.
모든 요청을 로그로 남기는 인터셉터를 개발한다.
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
public static final String LOG_ID = "logId";
// 싱글톤이라 여기서 prehandle코드 작성 불가
@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);
// @Controller가 아니라 정적 리소스가 호출되는 경우에는 : ResourceHttpRequestHandler
if (handler instanceof HandlerMethod){ // @RequestMapping의 경우 사용 되는 handler가 handlerMethod이다.
HandlerMethod hm = (HandlerMethod) handler; // 호출할 컨트롤러 메소드의 모든 정보가 포함되어 있다
}
log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
return true;
// true 다음 컨트롤러 호출
// false 여기서 끝남
}
@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);
if (ex != null) {
// 예외가 NUll이 아니면(예외처리를 여기서 하는 이유는 PostHandle이 호출되지 않는다)
log.error("afterCompletion error!!", ex); // 에러를 찍어볼 수 있음
}
}
}
WebConfig
에 등록
@Configuration
public class WebConfig implements WebMvcConfigurer { // implement함
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**") // 리소스 폴더 포함 하위의 모든 패턴
.excludePathPatterns("/css/**", "/*.ico", "/error");// 이 경로는 인터셉터 먹이지마
}
}
인증은 컨트롤러 호출 전에만 호출하면 되기 때문에, preHandle
만 구현하면 된다.
@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("미인증 사용자 요청");
//아래 코드 - 로그인하면 요청했던 페이지로 재이동가능
response.sendRedirect("/login?redirectURL=" + requestURI);
return false;
} return true;
}
}
registry.addInterceptor(new LoginCheckInterceptor())
.order(2)
.addPathPatterns("/**")
.excludePathPatterns(
"/", "/members/add", "/login", "/logout",
"/css/**", "/*.ico", "/error");
// 인터셉터의 장점: 패턴을 세밀하게 가져갈 수 있음
ArgumentResolver
를 활용하면 공통 작업이 필요할 때 컨트롤러를 더욱 편리하게 사용할 수 있다.
HomeController
에서 세션 대신에 @Login
을 추가한다.
직접 에노테이션을 만들어보자!
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {
//세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
@Login
애노테이션이 있으면 직접 만든 ArgumentResolver
가 동작해서 자동으로 세션에 있는 로그인 회원을 찾아주고, 만약 세션에 없다면 null
을 반환한다.
@Login
애노테이션 생성@Target(ElementType.PARAMETER) // 파라미터에만 사용
@Retention(RetentionPolicy.RUNTIME) // 리플렉션 등을 활용할 수 있도록 런타임까지 애노테이션 정보가 남아있음
public @interface Login {}
LoginMemberArgumentResolver
생성@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;
// @Login 애노테이션이 있으면서 Member 타입이면 해당 ArgumentResolver 가 사용된다.
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
log.info("resolveArgument 실행");
HttpServletRequest request = (HttpServletRequest)
webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}// 컨트롤러 호출 직전에 호출 되어서 필요한 파라미터 정보를 생성해준다.
// 여기서는 세션에 있는 로그인 회원 정보인 member 객체를 찾아서 반환해준다.
// 이후 스프링MVC는 컨트롤러의 메서드를 호출하면서 여기에서 반환된 member 객체를 파라미터에 전달해준다.
}
마지막으로, WebConfig
에 등록한다.
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
실행해보면, 결과는 동일하지만, 더 편리하게 로그인 회원 정보를 조회할 수 있다. 이렇게 ArgumentResolver
를 활용하면 공통 작업이 필요할 때 컨트롤러를 더욱 편리하게 사용할 수 있다.