[Spring] 로그인 처리2 - 필터, 인터셉터
필터, 인터셉터
로그인하지 않은 사용자가 로그인이 필요한 페이지에 접근할 경우 막아줄 수 있어야 한다. 그런 기능을 맡는 로직을
작성했을 때 여러 페이지에 대해서 해당 로직을 적용해야 한다면 유지 보수가 어려워진다. 이렇게 여러 기능에서 공통
으로 적용될 수 있는 내용을 공통 관심사라고 하고 서블릿에서는 필터로, 스프링에서는 인터셉터로 해결할 수 있다.
서블릿 필터
다음 과정을 통해 렌더링이 이뤄진다. 필터는 서블릿 호출 직전에 거치게 된다!
만약 필터에서 적절하지 않은 요청이라 판단된다면 서블릿을 호출하지 않는다.
여러 필터를 적용할 수 있으며 순서 또한 설정할 수 있다.
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러
필터 interface는 다음과 같다.
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()
: 필터 종료 메소드로, 서블릿 컨테이너 종료 시 호출
사용자가 로그인했는지 검증하는 필터를 만들어보자. 이름은 LoginCheckFilter
라 하자.
javax.servlet
의 Filter
를 구현하면 되고, init()
, destroy()
는 default 메소드 여서 반드시 구현하지 않아도 된다.
doFilter()
만 구현하면 다음과 같다.
public class LoginCheckFilter implements Filter {
//화이트 리스트: 인증 체크X
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 {
//체크 대상 페이지 진입
if(isLoginCheckPath(requestURI)) {
HttpSession session = httpRequest.getSession(false);
//미인증 사용자라면
if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
//로그인 페이지로 redirect
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
return;
}
}
chain.doFilter(request, response);
} catch(Exception e) {
throw e;
}
}
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}
}
isLoginCheckPath()
: 권한이 필요한 페이지인지 체크httpResponse.sendRedirect()
: 강제로 이동할 페이지를 설정?redirectURL=
를 설정함으로써 로그인 후requestURI
로 이동할 수 있도록 정보를 URL에 남김- 다음 필터 or 서블릿을 호출하지 않도록 리턴
chain.doFilter()
: 다음 필터 or 서블릿 호출 (반드시 호출해야 함)
LoginCheckFilter
는 다음과 같이 등록해서 사용할 수 있다!
WebMvcConfigurer
를 구현한 WebConfig
에서는 서블릿 필터, 스프링 인터셉터 모두 등록할 수 있다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
setFilter()
: 구현한 필터 등록setOrder()
: 필터 순서 설정addUrlPatterns()
: 필터 적용 범위 설정 ("/*"
: 모든 요청)
위에서 미인증 사용자의 경우 권한이 필요한 페이지 접근 시 redirect 처리를 했지만 로그인 후 ?redirectURL=
뒤
에 있는 requestURI
로 이동할 수 있도록 추가로 처리가 필요하다.
@PostMapping("/login")
public String loginV4(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request) {
...
return "redirect:" + redirectURL;
}
@RequestParam(defaultValue = "/")
: 위에서 설정한requestURI
값을 얻음(없으면 기본값"/"
)- 그렇게 얻은
redirectURL
로 redirect 처리 완료
스프링 인터셉터
서블릿 필터와 같은 기능을 스프링 MVC에서도 스프링 인터셉터라는 이름으로 제공한다.
모두 웹 관련 공통 관심 사항을 처리하지만 적용 순서, 기능이 서블릿 필터와 다르다.
일단 필터의 경우 서블릿 직전에 위치하지만 인터셉터는 컨트롤러 직전에서 호출된다.
인터셉터 또한 순서를 설정할 수 있다.
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러
다음은 스프링 인터셉터 interface이다.
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()
: 컨트롤러 호출 후. 에러 발생 시 호출X,modelAndView
정보 확인 가능afterCompletion()
: 뷰 렌더링 이후. 에러 발생 상관없이 반드시 호출, 에러 로그 확인 가능(ex
)- 모든 함수가 default여서 필요한 함수만 구현하면 됨
스프링 인터셉터를 활용한 로그인 체크 로직은 다음과 같다. 이름은 LoginCheckInterceptor
라 하자.
HandlerInterceptor
를 구현하도록 설정해준 뒤 preHandle()
에 로그인 검증 로직을 써보자.
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
HttpSession session = request.getSession();
//미인증 사용자라면
if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
//로그인 페이지로 redirect
response.sendRedirect("/login?redirectURL=" + requestURI);
return false;
}
return true;
}
}
- 서블릿 필터와 전체적인 로직은 유사함
- return
true
: 정상 호출. 다음 인터셉터 or 컨트롤러 호출false
: 다음 인터셉터 or 컨트롤러 호출X
WebConfig
에서 다음과 같이 스프링 인터셉터를 등록해줄 수 있다!
페이지 설정을 세세하게 할 수 있고, 여기서 설정을 해주기 때문에 서블릿 필터처럼 별도로 화이트리스트를 만들어
설정해줄 필요가 없다.
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");
}
}
addInterceptor()
: 구현한 스프링 인터셉터 등록order()
: 스프링 인터셉터 순서 설정addPathPatterns()
: 권한이 있어야 접근 가능한 페이지 설정excludePathPatterns()
:addPathPatterns()
에 설정한 페이지 중 예외 처리
(추가) ArgumentResolver
이전에 사용자의 세션이 유지되고 있는지 체크하기 위해 다음 코드를 사용했었다.
public String homeLoginV3Spring(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {
...
}
위 코드로도 정상적으로 동작하지만, 파라미터가 복잡하다는 단점이 있다.
ArgumentResolver를 통해 @Login
애노테이션을 만들어서 적용하면 훨씬 편리하게 처리할 수 있다!
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {
...
}
일단 @Login
애노테이션 정의를 다음과 같이 해주자.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
@Target(ElementType.PARAMETER)
: 파라미터에만 사용@Retention(RetentionPolicy.RUNTIME)
: 런타임까지 애노테이션 정보가 남아있음 (일반적으로 사용)
@Login
처리를 통해 loginMember가 적절한 정보를 얻을 수 있도록 설정해주는 코드이다.
HandlerMethodArgumentResolver
를 구현하도록 설정해주자.
public class LoginMemberArugmentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
//@Login이 있는가?
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
//Member 타입인가?
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if(session == null) {
return null;
}
//세션에서 로그인 회원 정보를 찾아 반환
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
supportsParameter()
: @Login이 있고 Member 타입인지 체크resolveArgument()
: 세션에서 로그인 회원 정보 반환
WebConfig
에서 생성한 ArgumentResolver LoginMemberArugmentResolver
를 등록해주자.
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArugmentResolver());
}
}
서블릿 필터든 스프링 인터셉터든 공통 관심 사항을 처리할 수 있으나 스프링 인터셉터가 사용하는 코드가 적고
관리하기 편하다는 장점이 있어서 특별한 이유가 없는 이상 스프링 인터셉터를 많이 사용한다고 한다!
댓글남기기