본 게시물은 스스로의 공부를 위한 글입니다.
틀린 내용이 있을 수 있습니다.
웹 페이지가 로그인된 사용자에게만 들어갈 수 있다고 해보자.
그럼 모든 컨트롤러에 로그인 여부를 확인하는 코드를 짜야하는데... 코드 중복도 많이진 뿐더러, 로그인 로직이 바뀌게되면 작성한 모든 로직을 수정해야 한다.
이렇게 애플리케이션 여러 로직에서 공통으로 관심이 있는 있는 것을 공통 관심사(cross-cutting concern)라고 한다.
이러한 공통 관심사는 스프링의 AOP로도 해결할 수 있지만, 웹과 관련된 공통 관심사는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다
필터 흐름
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
필터체인: HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러
필터에서 로직을 통해 서블릿을 호출하지 않을 수 있다.
필터 구현 로직 중 chain.doFilter(request, response)
를 넣으면 필터를 체인으로 수행할 수 있다. 호출할 추가 필터가 없으면 서블릿이 뜬다. 반드시 넣어야 한다. 안넣으면 다음 진행이 안된다.
Filter
을 이용한다.public default void init
: 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.public void doFilter
:고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.public default void destroy
: 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.@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 {
if (isLoginCheckPath(requestURI)) {
HttpSession session = httpRequest.getSession(false);
if (session == null || session.getAttribute("login") == 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
로 인증 체크를 하고싶지 않은 url을 등록한다.@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter()); //내가 구현한 필터 넣기
filterRegistrationBean.setOrder(1); //필터 체인할 때 가장 먼저 실행
filterRegistrationBean.addUrlPatterns("/*"); //모든 요청 url에 대해 실행
return filterRegistrationBean;
}
}
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
스프링 인터셉터 제한
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출 X) // 비 로그인 사용자
스프링 인터셉터 체인
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러
HandlerInterceptor
인터페이스를 구현하면 된다.default boolean preHandle
: 컨트롤러 호출 전 호출default void postHandle
: 컨트롤러 호출 후에 호출default void afterCompletion
: 요청 완료 이후Exception
파라미터로 예외를 받을 수 있음(정상 흐름에선 null 파라미터)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(false);
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("미인증 사용자 요청");
//로그인으로 redirect
response.sendRedirect("/login?redirectURL=" + requestURI);
return false;
}
return true;
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginCheckInterceptor()) //인터셉터 등록. 여기서 LoginCheckInterceptor()은 내가 구현한 클래스 이름이다.
.order(1) //낮을 수록 먼저 호출
.addPathPatterns("/**") //인터셉터를 적용할 url 패턴
.excludePathPatterns("/css/**", "/*.ico", "/error"); //인터셉터에서 제외할 패턴 지정
}
}
@GetMapping("/")
public String login(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {
if (loginMember == null) { //세션에 없는 사용자.
return "login";
}
//로그인된 회원
model.addAttribute("member", loginMember);
return "home";
}
음.. 좀 길지 않나? 이걸 사용할 컨트롤러마다 다 쓸 생각하니 머리가 어질😵
ArgumentResolver
을 사용하자! 그럼 아래와 같은 코드로 받을 수 있다.@GetMapping("/")
public String login(@Login Member loginMember, Model model) {
if (loginMember == null) { //세션에 없는 사용자.
return "login";
}
//로그인된 회원
model.addAttribute("member", loginMember);
return "home";
}
@Login
애노테이션이 있으면 직접 만든 ArgumentResolver
가 동작해서 자동으로 세션에 있는 로그인 회원을 찾아주고, 만약 세션에 없다면 null
을 반환하도록 개발해보자.@Login
애노테이션 생성@Target(ElementType.PARAMETER) //파라미터에만 사용할겁니다.
@Retention(RetentionPolicy.RUNTIME) //런타임까지 애노테이션 정보가 남아있게 하기위해.
public @interface Login {
}
HandlerMethodArgumentResolver
구현@Slf4j
public class LoginMemberArgumentResolver implementsHandlerMethodArgumentResolver {
@Override
//resolveArgument를 실행하기 위한 조건
//@Login 애노테이션이 존재 && Member 타임이여야 한다.
public boolean supportsParameter(MethodParameter parameter) {
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
@Override
//세션에 있는 member 객체를 찾아서 반환. 못찾으면 null 반환
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);
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
}
인프런의 '스프링 MVC 2편(김영한)'을 스스로 정리한 글입니다.
자세한 내용은 해당 강의를 참고해주세요