이번 포스팅에서는 Spring Boot
애플리케이션에서 IP 접근 제한
하는 방법에 대해 설명합니다. IP 접근 제한은 DB에 White List
를 추가하고, Interceptor
에서 IP 검증 후, 접근할 수 있도록 설정할 예정입니다.
우선 DB 테이블에 담길 Entity
를 생성합니다.
WhiteList 테이블에는 간략하게, IP 정보와 접근 일시를 추가하였습니다.
@Entity(name = "tb_whitelist")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor(force = true)
@Getter
@SequenceGenerator(name = "SEQ_ACCESS_IP_GENERATOR", sequenceName = "SEQ_ACCESS_IP", initialValue = 1, allocationSize = 1)
public class WhiteIp {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_ACCESS_IP_GENERATOR")
@Column(name = "rec_key", columnDefinition = "int8 DEFAULT nextval('SEQ_ACCESS_IP'::regclass)")
private Long recKey;
@Column(name = "access_ip", length = 20)
private String accessIp;
@Column(name = "access_date")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime accessDate;
}
Entity의 DAO 역할을 할 Repository를 생성합니다.
@Repository
public interface WhiteIpRepository extends CrudRepository<WhiteIp, Long> {
Optional<WhiteIp> findByAccessIp(String ip);
}
프로그램 실행시 아래 옵션을 추가하면 Client IP를 추적할 수 있습니다.
-Djava.net.preferIPv4Stack=true
public class Utils {
public static String getClientIP(HttpServletRequest request) {
String[] headers = {"Proxy-Client-IP",
"WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR",
"X-Real-IP", "X-RealIP", "REMOTE_ADDR"};
String ip = request.getHeader("X-Forwarded-For");
for (String header : headers) {
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader(header);
}
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if(ip.equals("0:0:0:0:0:0:0:1")){
ip = "127.0.0.1";
}
return ip;
}
}
HandlerInterceptor
인터페이스를 상속 받아, IP 접근 제한을 수행하는 IpAccessInterceptor
클래스를 작성합니다.
...
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@RequiredArgsConstructor
@Slf4j
public class IpAccessInterceptor implements HandlerInterceptor {
private final WhiteIpRepository whiteIpRepository;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
String clientIp = Utils.getClientIP(request);
if (clientIp.equals("127.0.0.1")) {
// 로컬 접속이면 당연히 true
return true;
}
if (!whiteIpRepository.findByAccessIp(clientIp).isPresent()) {
log.warn("Forbidden access, URI: {}, IP: {}", request.getRequestURI(), clientIp);
response.sendError(403, "IP Forbidden");
return false;
}
return true;
}
}
WebMvcConfigurer
를 이용하여 Interceptor
를 등록합니다.
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final IpAccessInterceptor ipAccessInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(ipAccessInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/static/**")
.excludePathPatterns("/error/**")
.excludePathPatterns("/api/**");
}
}
이제 페이지에 접근하시면 에러페이지가 나오는 것을 볼수 있습니다.
Interceptor
를 사용하기 전에 참고하면 좋을 만한 내용들에 대해 정리해 보았습니다.
인터셉터는 컨트롤러(Controller)의 핸들러(Handler)를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 일종의 필터입니다.
Interceptor는 Interrupt에서 파생된 단어인데, '낚아채다'의 의미를 갖고 있습니다.
스프링이 제공해주는 HandlerInterceptor 인터페이스와 HandlerInterceptorAdapter 추상클래스에 정의되어 있는 메서드는 preHandle(), postHandle(), afterCompletion() 3가지입니다.
preHandle()
: 컨트롤러가 실행 이전에 처리해야 할 작업이 있는경우 혹은 요청정보를 가공하거나 추가하는경우 사용한다.postHandle()
: 핸들러가 실행은 완료되었지만 아직 View가 생성되기 이전에 호출된다.afterCompletion()
: 모든 View에서 최종 결과를 생성하는 일을 포함한 모든 작업이 완료된 후에 실행된다.웹 프로그램을 개발하다 보면 로깅, 로그인 관련(세션체크)처리, 권한체크, XSS 방어, 페이지 인코딩 변환 등 공통으로 처리해야 될 로직들이 많습니다.
이 때, 스프링에서는 이러한 공통 로직들을 로직의 앞, 중간, 뒤에 추가하여 자동으로 처리할 수 있는 3가지 방법(Filter, Interceptor, AOP)이 있습니다.
Filter는 요청과 응답을 거른뒤 정제하는 역할을 수행합니다.
서블릿 필터는 DispatcherServlet 이전에 실행이 되는데 필터가 동작하도록 지정된 자원의 앞단에서 요청내용을 변경하거나, 여러가지 체크를 수행할 수 있다.
일반적으로 Encoding 변환처리, XSS 방어 등의 요청에 대한 처리로 사용합니다.
실행 메소드로는 init(), doFilter(), destory()가 있습니다.
요청에 대한 작업 전/후로 가로챈다고 보면 됩니다.
필터는 스프링 컨텍스트 외부에 존재하여 스프링과 무관한 자원에 대해 동작하지만, 인터셉터는 스프링의 DistpatcherServlet이 컨트롤러를 호출하기 전, 후로 끼어들기 때문에 스프링 컨텍스트(Context, 영역) 내부에서 Controller(Handler)에 관한 요청과 응답에 대해 처리합니다.
AOP는 주로 로깅
, 트랜잭션
, 에러 처리
등 비즈니스단의 메서드에서 조금 더 세밀하게 조정하고 싶을 때 사용합니다.
Interceptor나 Filter와는 달리 메소드 전후의 지점에 자유롭게 설정이 가능하고, Filter는 주소로 대상을 구분해서 걸러내야하는 반면, AOP는 주소, 파라미터, 애노테이션 등 다양한 방법으로 대상을 지정할 수 있습니다.
AOP의 포인트컷은 다음과 같습니다.
@Before
: 대상 메서드의 수행 전@After
: 대상 메서드의 수행 후@After-returning
: 대상 메서드의 정상적인 수행 후@After-throwing
: 예외발생 후@Around
: 대상 메서드의 수행 전/후이번 포스팅에서는 IP 접근제한을 위해 Interceptor를 작성하고, 등록하는 방법에 대한 프로세스에 대해 설명하였습니다.
또한 Spring에서 공통 로직을 처리하는 방법에 대해 간략하게 정리해보았는데, 이 내용들은 Spring 동작원리, Core에 해당하는 부분이라 차후 세부적으로 정리해봐도 좋을 거 같은 토픽이라 생각됩니다.
이상으로 이번 포스팅을 마치겠습니다. 읽어주셔서 감사합니다🥰