빙응의 공부 블로그
[Spring]스프링 MVC 2편 - 로그인 처리(필터, 인터셉터) 본문
📝필터와 인터셉터
우리가 바로 전 포스팅에서 보안에 신경쓴 웹 사이트는 한가지 큰 문제가 있다.
그것은 바로 로그인을 안해도 URL을 통해 사이트에 접근이 가능하다.
상품 관리 컨트롤러에서 로그인 여부를 체크하는 로직을 작성해야한다.
그러나 모든 컨트롤러에 해당 로직을 넣어야 하는 문제가 발생한다.
이것을 로직에서 공통으로 관심이 있는 것으로 공통 관심사라고 한다.
이러한 공통 관심사는 스프링의 AOP로 해결할 수 있지만, 웹과 관련된 공통 관심사는 지금부터 설명할
서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다.
서블릿 필터
필터는 서블릿이 지원하는 수문장 역할이다.
로직이라 생각하면 편하다.
필터 흐름
HTTP 요청 - WAS - 필터 - 서블릿 - 컨트롤러
필터를 적용하면 필터가 호출된 다음에 서블릿이 호출된다.
그래서 모든 고객의 요청 로그를 남기는 요구사항이 있다면 필터를 사용하면 된다. 특정 URL 패턴에 적용도 가능하다.
필터 제한
로그인한 사용자만 접근하게 로직을 만들 수 있다.
HTTP 요청 - WAS - 필터 - 서블릿 -컨트롤러 //로그인
HTTP 요청 - WAS - 필터 //비 로그인
필터 체인
필터는 여러개 사용이 가능하다.
HTTP 요청 - WAS - 필터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() : 필터 종료 메서드, 서블릿 컨테이너가 종료될 대 호출
📝필터 적용해보기 - 로그 남기기
필터가 정말 수문장 역할을 잘하는지 확인하기 위해 가장 단순한 필터인, 모든 요청을 로그로 남기는 필터를 개발해보자
@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");
//ServletRequest는 HttpServletRequest의 부모로 기능이 적어 다운캐스팅한다.
HttpServletRequest httrequest = (HttpServletRequest) request;
String requestURI = httrequest.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");
}
}
doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- HTTP 요청이 오면 doFilter가 호출된다.
chain.doFilter(request, respons)
- 이 부분이 가장 중요하며, 다음 필터가 있는 필터를 호출하고, 필터가 없으면 서블릿을 호출한다.
- 만약 이 로직이 없으면 다음 단계로 진행되지 않는다.
하지만 이 상태로는 필터를 사용할 수 없다.
스프링 필터 설정 방법
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = newFilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
필터를 등록하는 방법은 여러가지 있지만, 스프링 부트를 사용한다면 FilterRegistrationBean을 사용하면 된다.
- SetFilter(new LogFilter()) : 등록할 필터를 지정한다.
- SetOrder(1) : 필터는 체인으로 동작한다. 순서를 정한다.
- addUrlPatterns() : 필터를 적용할 URL 패턴을 지정할 수 있다.
참고! 실무에서 HTTP 요청시 같은 요청의 로그에 모두 같은 식별자를 자동으로 남기는 방법은 logback mdc이다.
📝필터 적용해보기 - 인증 체크
로그인 되지 않은 사용자는 상품 관리 뿐만 아니라 미래에 개발될 페이지에도 접근 못하도록 해보자.
@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;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String requestURI = httpRequest.getRequestURI();
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);
}
}
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
천천히 살펴보자
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;
}
}
해당 코드가 실질적인 인증 코드이다.
- if(isLoginCheckPath(requestURI))
- 요청 URI가 인증 없이 접근 가능한 URI인지 검사하는 것이다.
- 인증이 필요 없는 곳은 굳이 체크를 안하고 바로 chain을 하면된다.
- HttpSession session = httpRequest.getSession(false);
- 해당 로직은 전 포스팅에서 보았듯이 세션을 조회하는 로직이다.
- if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null)
- 세션이 없으면 미인증 사용자로 간주하여 로그인 페이지로 이동시키는 것
- httpResponse.sendRedirect("/login?redirectURL="+requestURI);
- 다른 페이지로 리다이렉트 시키는 것으로 로그인 페이지로 이동시킨다.
📝스프링 인터셉터
필터는 서블릿이 제공하고 스프링 인터셉터는 스프링에서 제공하는 기능이다.
스프링 인터셉터 흐름
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤
- 스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출된다.
- 스프링 인터셉터는 스프링 MVC가 제공하는 기능이기 때문에 결국 디스패처 서블릿 이후에 등장하게 된다.
- 스프링 인터셉터도 URL 패턴을 적용할 수 있고 매우 정밀하게 설정이 가능하다.
스프링 인터셉터 인터페이스
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 {}
}
서블릿 필터의 경우 단순하게 doFilter() 하나만 제공된다. 인터셉터는 컨트롤러 호출 전, 호출 후, 요청 완료 이후와 같이 단계적으로 잘 세분화 되어있다.
- proHandle : 컨트롤러 호출 전
- postHandle : 컨트롤러 호출 후
- afterCompletion : 요청 완료 이후
서블릿 필터의 경우 단순히 request, response 만 제공했지만, 인터셉터는 어떤 컨트롤러가 호출되는지 호출 정보도 받을 수 있다. 그리고 어떤 modelAndView가 반환되는지 응답 정보도 받을 수 있다.
흐름
- preHandle : 컨트롤러 호출 전에 호출된다.
- preHandle 의 응답 값이 true이면 다음으로 진행하고, false이면 더는 진행하지 않는다. false인 경우 핸들러 어댑터도 호출 X
- postHandle : 컨트롤러 호출 후에 호출된다.
- afterCompletion : 뷰가 렌더링 된 이후에 호출된다.
예외 상황
- postHandle은 preHandle이나 컨트롤러에 예외가 발생하면 실행되지 않는다.
- afterCompletion은 항상 호출된다. 예외가 발생하면 어떤 예외가 발생했는지 파라미터로 받을 수 있다.
@Slf4j
public class LogInterseptor 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 logId = (String)request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}]", logId, requestURI);
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
하나하나 살펴보자
request.setAttribute(LOG_ID,uuid);
- 서블릿 필터의 경우 지역변수로 해결이 가능하지만, 스프링 인터셉터는 호출 시점이 완전히 불리되어 있어 지역변수 사용이 제한된다. 또한 싱글톤처럼 사용되기 때문에 주의하여 request에 넣고 찾아서 사용하는 형태가 좋다.
return true;
- preHandler에서 true는 정상호출을 뜻한다. 다음 인터셉터나 컨트롤러가 호출된다.
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
}
스프링 MVC 1편에서 했던 핸들러에 대한 메소드이다.
HandlerMethod
핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라진다. 스프링을 사용하면 일반적으로 @Controller , @RequestMapping 을 활용한 핸들러 매핑을 사용하는데, 이 경우 핸들러 정보로 HandlerMethod 가 넘어온다.
ResourceHttpRequestHandler
@Controller 가 아니라 /resources/static 와 같은 정적 리소스가 호출 되는 경우
ResourceHttpRequestHandler 가 핸들러 정보로 넘어오기 때문에 타입에 따라서 처리가 필요하다.
postHandle, afterCompletion
종료 로그를 postHandle 이 아니라 afterCompletion 에서 실행한 이유는,
예외가 발생한 경우 postHandle 가 호출되지 않기 때문이다. afterCompletion 은 예외가 발생해도 호출 되는 것을 보장한다
인터셉터 스프링 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterseptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
}
}
WebMvcConfigurer 가 제공하는 addInterceptors() 를 사용해서 인터셉터를 등록할 수 있다
- registry.addInterceptor(new LogInterceptor()) : 인터셉터 등록
- order(1) : 호출 순서를 지정
- addPathPatterns() : 인터셉터를 적용할 URL 패턴 적용
- excludePathPatterns("/css/**", "/*.ico", "/error") : 인터셉터에서 제외할 패턴을 지정
PathPattern은 따로 규칙이 있으니 공식 사이트를 보자
PathPattern (Spring Framework 6.1.3 API)
PathPattern (Spring Framework 6.1.3 API)
Compare this pattern with a supplied pattern: return -1,0,+1 if this pattern is more specific, the same or less specific than the supplied pattern.
docs.spring.io
📝스프링 인터셉터 - 인증 체크
@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;
}
}
@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"
);
}
위 코드를 보면 필터보다 훨씬 간결해진 것을 알 수 있다.
📝ArgumentResolver 활용
MVC 1편에서 요청 매핑 핸들러 어뎁터 구조(ArgumentResolver)에 대해 학습하였다.
ArgumentResolver
파라미터를 유연하게 처리해주는 컨트롤러로 애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdapter는
ArgumentResolver를 사용하여 다양한 파라미터 값을 생성한다.
해당 기능을 사용해서 로그인 회원을 조금 편리하게 찾아보자
@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("resolveArgument 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
해당 코드를 천천히 살펴보자
HandlerMethodArgumentResolver는 2가지 메소드가 오버라이딩된다.
- supportsParameter 메서드: 이 메서드는 파라미터가 해당 HandlerMethodArgumentResolver에 의해 지원되는지 여부를 판단한다. 즉, 이 메서드가 true를 반환하면 resolveArgument 메서드가 호출된다.
- resolveArgument 메서드: supportsParameter가 true를 반환하면 이 메서드가 호출되어 실제로 파라미터를 해결한다.
위의 코드는 supportsParameter를 통해 지원 여부를 확인하고 resolveArgument를 통해 세션을 받아온다.
✔정리
우리는 공통 관심사를 해결하는 필터와 인터셉터에 대해 알아보았다. 이것을 알아본 이유는 Spring Security의 기반 기술이기 때문이다. (필터 해당) 세션을 사용하면 필터를 통해 접근 제한을 하는 기술이다. 기반 기술을 알아야 제어, 오류가 쉽기 때문에 필터에 대한 이해가 필요하다.
또한 ArgumentResolver는 현업에서도 간단히 사용할 수 있는 것이라 하였다. 응용해보아도 좋을 것 같다.
'Spring > 인프런_개념' 카테고리의 다른 글
[Spring]스프링 MVC 2편 - API 오류 처리 (0) | 2024.02.06 |
---|---|
[Spring]스프링 MVC 2편 - 예외 처리와 오류 페이지 (1) | 2024.02.05 |
[Spring]스프링 MVC 2편 - 쿠키와 보안 (1) | 2024.02.01 |
[Spring]스프링 MVC - 검증(Validation) (0) | 2024.01.28 |
[Spring]스프링 MVC 2편 - 메시지, 국제화 (1) | 2024.01.24 |