[Spring Boot] IP 접근 제한 구현

김희정·2023년 11월 13일
1

Spring

목록 보기
10/18

💎 들어가며

이번 포스팅에서는 Spring Boot 애플리케이션에서 IP 접근 제한하는 방법에 대해 설명합니다. IP 접근 제한은 DB에 White List를 추가하고, Interceptor에서 IP 검증 후, 접근할 수 있도록 설정할 예정입니다.



1. DB 구축

1.1 JPA Entity 생성

우선 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;
}

1.2 JPA Repository 생성

Entity의 DAO 역할을 할 Repository를 생성합니다.

@Repository
public interface WhiteIpRepository extends CrudRepository<WhiteIp, Long> {
    Optional<WhiteIp> findByAccessIp(String ip);
}

2. Interceptor 생성하기

2.1 Client IP 알아내기

java 실행 옵션 추가

프로그램 실행시 아래 옵션을 추가하면 Client IP를 추적할 수 있습니다.

-Djava.net.preferIPv4Stack=true

Client IP 알아내기

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;
    }
}

2.2 Interceptor 클래스 작성

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;
    }
}

2.3 Interceptor 등록

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/**");
    }
}

이제 페이지에 접근하시면 에러페이지가 나오는 것을 볼수 있습니다.


3. Interceptor

Interceptor를 사용하기 전에 참고하면 좋을 만한 내용들에 대해 정리해 보았습니다.

3.1 Interceptor란?

Interceptor 정의

인터셉터는 컨트롤러(Controller)의 핸들러(Handler)를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 일종의 필터입니다.

Interceptor는 Interrupt에서 파생된 단어인데, '낚아채다'의 의미를 갖고 있습니다.

Interceptor Method

스프링이 제공해주는 HandlerInterceptor 인터페이스와 HandlerInterceptorAdapter 추상클래스에 정의되어 있는 메서드는 preHandle(), postHandle(), afterCompletion() 3가지입니다.

  1. preHandle(): 컨트롤러가 실행 이전에 처리해야 할 작업이 있는경우 혹은 요청정보를 가공하거나 추가하는경우 사용한다.
  2. postHandle(): 핸들러가 실행은 완료되었지만 아직 View가 생성되기 이전에 호출된다.
  3. afterCompletion(): 모든 View에서 최종 결과를 생성하는 일을 포함한 모든 작업이 완료된 후에 실행된다.

Interceptor 의 동작 위치 및 순서

nterceptor 의 동작 위치 및 순서

  1. 사용자는 서버에 자신이 원하는 작업을 요청하기 위해 url을 통해 Request 객체를 보낸다.
  2. DispatcherServlet은 해당 Request 객체를 받아서 분석한뒤 '핸들러 매핑(HandlerMapping)' 에게 사용자의 요청을 처리할 핸들러를 찾도록 요청 한다.
  3. 그결과로 핸들러 실행체인(HandlerExectuonChanin)이 동작하게 되는데, 이 핸들러 실행 체인은 하나이상의 핸들러 인터셉터를 거쳐서 컨트롤러가 실행될수 있도록 구성되어 있다.
    (핸들러 인터셉터를 등록하지 않았다면, 곧바로 컨트롤러가 실행된다. 반대로 하나이상의 인터셉터가 지정되어 있다면 지정된 순서에 따라서 인터셉터를 거쳐서 컨트롤러를 실행한다)

3.2 Filter vs Interceptor vs AOP

웹 프로그램을 개발하다 보면 로깅, 로그인 관련(세션체크)처리, 권한체크, XSS 방어, 페이지 인코딩 변환 등 공통으로 처리해야 될 로직들이 많습니다.

이 때, 스프링에서는 이러한 공통 로직들을 로직의 앞, 중간, 뒤에 추가하여 자동으로 처리할 수 있는 3가지 방법(Filter, Interceptor, AOP)이 있습니다.

Fitler vs Interceptor vs AOP

Filter

Filter는 요청과 응답을 거른뒤 정제하는 역할을 수행합니다.

서블릿 필터는 DispatcherServlet 이전에 실행이 되는데 필터가 동작하도록 지정된 자원의 앞단에서 요청내용을 변경하거나,  여러가지 체크를 수행할 수 있다.

일반적으로 Encoding 변환처리, XSS 방어 등의 요청에 대한 처리로 사용합니다.

실행 메소드로는 init(), doFilter(), destory()가 있습니다.

  • init() - 필터 인스턴스 초기화
  • doFilter() - 전/후 처리
  • destroy() - 필터 인스턴스 종료

Interceptor

요청에 대한 작업 전/후로 가로챈다고 보면 됩니다.

필터는 스프링 컨텍스트 외부에 존재하여 스프링과 무관한 자원에 대해 동작하지만, 인터셉터는 스프링의 DistpatcherServlet이 컨트롤러를 호출하기 전, 후로 끼어들기 때문에 스프링 컨텍스트(Context, 영역) 내부에서 Controller(Handler)에 관한 요청과 응답에 대해 처리합니다.


AOP

AOP는 주로 로깅, 트랜잭션, 에러 처리등 비즈니스단의 메서드에서 조금 더 세밀하게 조정하고 싶을 때 사용합니다.

Interceptor나 Filter와는 달리 메소드 전후의 지점에 자유롭게 설정이 가능하고, Filter는 주소로 대상을 구분해서 걸러내야하는 반면, AOP는 주소, 파라미터, 애노테이션 등 다양한 방법으로 대상을 지정할 수 있습니다.

AOP의 포인트컷은 다음과 같습니다.

  • @Before: 대상 메서드의 수행 전
  • @After: 대상 메서드의 수행 후
  • @After-returning: 대상 메서드의 정상적인 수행 후
  • @After-throwing: 예외발생 후
  • @Around: 대상 메서드의 수행 전/후

💎 Reference


💎 마치며

이번 포스팅에서는 IP 접근제한을 위해 Interceptor를 작성하고, 등록하는 방법에 대한 프로세스에 대해 설명하였습니다.

또한 Spring에서 공통 로직을 처리하는 방법에 대해 간략하게 정리해보았는데, 이 내용들은 Spring 동작원리, Core에 해당하는 부분이라 차후 세부적으로 정리해봐도 좋을 거 같은 토픽이라 생각됩니다.

이상으로 이번 포스팅을 마치겠습니다. 읽어주셔서 감사합니다🥰

profile
Java, Spring 기반 풀스택 개발자의 개발 블로그입니다.

0개의 댓글