본문 바로가기
Back-End/🌱 Spring Boot (java)

[Spring] 서블릿 인터셉터 개념과 로그인(인증) 필터 구현

by 코딩하는 동현😎 2023. 5. 21.

로그인 기능을 구현해도 로그인된 사용자에 맞춰서 기능을 구현하는 것 까진 잘되는데, 특정 페이지에 접근하는 것을 막는것은 불가능합니다.

예를 들면 로그인하지 않은 사람이 '회원정보 수정' 페이지를 들어갈수 있는 버튼이 뜨지 않도록 페이지를 만들지만, 회원정보 수정 URL만 있으면 접근자체는 백엔드에서 막지 않았으므로 언제든지 url로 접근이 가능합니다.

 

컨트롤러에서 각 요청마다 로그인 여부를 체크하는 로직을 하나하나 작성하면 되겠지만, 수많은 컨트롤러마다 로직을 작성하는 것은 정말 비효율적이고, 나중에 로그인과 관련된 로직이 바뀌는 순간 모든 컨트롤러에 적용시킨 로그인 여부 체크 로직을 일일히 하나하나 바꿔야 되므로 유지보수가 어렵습니다.

 

이런 관심사는 스프링의 AOP를 이용해서 해결할수 있지만, 서블릿 필터나 스프링 인터셉터를 사용하는 것이 더 편리합니다.

웹과 관련된 공통 관심사를 처리할 때 Http 헤버나 URL정보들이 필요한데, HttpServletRequest를 이용해서 조회할 수 있습니다.

 

저번 포스트에서 스프링 필터를 이용해서 구현했는데, 이번 포스트에서는 스프링 인터셉터를 이용해서 구현해보겠습니다.


서블릿 인터셉터

스프링 인터셉터도 서블릿 필터처럼 웹과 관련된 관심 사항을 해결할 수 있습니다.

서블릿 필터가 서블릿이 제공하는 기술이라면, 인터셉터는 스프링 mvc가 제공하는 기술입니다.

 

스프링 인터셉터의 흐름

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러

 

스프링 인터셉터도 URL패턴을 적용할 수 있는데, 서블릿 필터보다 더 정밀하게 설정 할 수 있습니다.

 

스프링 인터셉터 체인

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러

스프링 인터셉터는 체인으로 구성되는데, 중간에 인터셉터를 자유롭게 추가할 수 있습니다. 예를 들어서 로그를 남기는 인터셉터를 먼저 적용하고, 그 다음에 로그인 여부를 체크하는 인터셉터를 만들 수 있습니다.


스프링 인터셉터 인터페이스

스프링의 인터셉터를 사용하려면 HandlerInterceptor 인터페이스를 받아와서 구현(implement) 하면 됩니다.

 

 HandlerInterceptor

public interface HandlerInterceptor {

	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		return true;
	}

	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 ), 요청 완료 이후( afterCompletion )와 같이 단계적으로 잘 세분화 되어 있습니다.

서블릿 필터의 경우 단순히 request , response 만 제공했지만, 인터셉터는 어떤 컨트롤러( handler )가 호출되는지 호출 정보도 받을 수 있습니다. 그리고 어떤 modelAndView 가 반환되는지 응답 정보도 받을 수 있습니다.

 


요청 로그 인터셉터 구현하기

interceptor폴더를 만들고, LogInterceptor파일을 만듭니다.

그리고 위에서 소개한 HandlerInterceptor를 구현(implements)해서 preHandle, postHandle, afterCompletion 메서드를 오버라이딩해서 받아옵니다.

 

interceptor/LogInterceptor.java

package jpabook.jpastore.interceptor;

import java.util.UUID;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class LogInterceptor implements HandlerInterceptor{

  public static final String LOG_ID = "logId";
  
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    String requestURL = 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, requestURL, handler);

    return true; // false 하면 여기서 중단.
  }
  
  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
      ModelAndView modelAndView) throws Exception {
    log.info("REQUEST [{}]", modelAndView);
  }
  
  
  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
      throws Exception {
    String requestURL = request.getRequestURI();
    String logId = (String) request.getAttribute(LOG_ID);
    log.info("RESPONSE [{}][{}][{}]", logId, requestURL, handler);
    if (ex != null) {
      log.error("afterComletion error!!", ex);
    }
  }
}

 

HandlerMethod

@Controller, @RequestMapping을 활용한 핸들러 매핑을 사용했을때, 그 정보가 HandlerMethod으로 넘어옵니다.

 

ResourceHttpRequestHandler

@Controller가 아니라 /resources/static와 같이 정적 리소스가 호출되는 경우 ResourceHttpRequestHandler로 정보가 넘어오기 때문에 처리가 필요합니다.


환경변수 파일로 인터셉터 등록하기

@Configuration 어노테이션을 등록하면 환경변수 파일이 됩니다.

WebMvcConfigurer를 구현(implements)을 해서 WebMvcConfigurer가 제공하는 addInterceptors() 를 사용해서 인터셉터를 등록할 수 있습니다.

 

WebConfig.java

package jpabook.jpastore;

import javax.servlet.Filter;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import jpabook.jpastore.interceptor.LogInterceptor;

@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LogInterceptor())
            .order(1)
            .addPathPatterns("/**")
            .excludePathPatterns("/css/**" , "/*.ico", "/error");
  }

}

addPathPatterns("/**") : 인터셉터를 적용할 URL 패턴을 지정합니다.

excludePathPatterns("/css/**", "/*.ico", "/error") : 인터셉터에서 제외할 패턴을 지정합니다.

필터와 비교해보면 인터셉터는 addPathPatterns , excludePathPatterns 로 정규식까지 활용해서 매우 정밀하게 URL 패턴을 지정할 수 있습니다.

 

PathPattern의 예시

  • ? 한 문자 일치
  • * 경로(/) 안에서 0개 이상의 문자 일치
  • ** 경로 끝까지 0개 이상의 경로(/) 일치
  • {spring} 경로(/)와 일치하고 spring이라는 변수로 캡처
  • {*spring} 경로가 끝날 때 까지 0개 이상의 경로(/)와 일치하고 spring이라는 변수로 캡처

PathPattern 공식 문서

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html


로그인 인증 체크 인터셉터

스프링 서블릿 내장 세션방식으로 로그인을 구현했을때를 가정하고 작성했습니다.

interceptor폴더를 안에다가 LoginCheckInterceptor파일을 생성하고 인터페이스를 구현합니다.

 

인증이라는 것은 컨트롤러 호출 전에만 호출되면 되므로 preHandle 메서드만 오버라이딩해서 구현하면 됩니다.

 

interceptor/LoginCheckInterceptor.java

package jpabook.jpastore.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.web.servlet.HandlerInterceptor;

import lombok.extern.slf4j.Slf4j;

@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("memberId") == null) {
      log.info("미인증 사용자 요청 {}", requestURI);
      // 로그인으로 redirect
      response.sendRedirect("/login?redirectURL="+ requestURI);
      return false;
    }

    return true;
  }
  
}

인증 체크 인터셉터도 등록하기

order를 2로 했으므로 1로 해논 요청로그 인터셉터 다음에 호출됩니다.

 

WebConfig.java

package jpabook.jpastore;

import javax.servlet.Filter;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import jpabook.jpastore.filter.LogFilter;
import jpabook.jpastore.filter.LoginCheckFilter;
import jpabook.jpastore.interceptor.LogInterceptor;
import jpabook.jpastore.interceptor.LoginCheckInterceptor;

@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", "/test");
  }
}

실행결과

 

 

 

 

반응형

댓글