Backend/Spring

해외 IP 차단

Jeffrey Oh 2024. 6. 25. 18:43
반응형

가끔 중국에서 phpAdmin 뭐시기로 자꾸 조회를 하길래 로그 보기 싫어서 아예 차단하는 로직을 추가하기로 했다.


MaxMind Database

MaxMind 라는 회사에서 국가별 IP 대역 Database 를 제공한다. (구글링하니까 전부 이 데이터를 쓰더라..)

여기서 회원가입을 먼저 해야하는데 뭔가 좀 불친절하다. 그래서 바로 링크 남겨 놓는다.

MaxMind 회원가입 링크 (레퍼럴 이딴거 없으니까 의심하지 말고 누르고 회원가입 하시길..)

회원가입이 완료되면 가입한 이메일로 인증 메일이 전송되며 링크를 클릭하면 비밀번호를 설정하라고 나온다.
비밀번호 설정 후 로그인을 하면 처음에 마이페이지 같은 페이지로 접속이 된다.
만약 아니라면 우측 상단의 My Account -> My Account 눌러!요..

그러면 사이드 메뉴 중 GeoIP2 / GeoLite2 에서 Download Files 메뉴를 클릭하고 다음을 찾아서 다운로드 받는다.
GeoLite2 Country -> Download GZIP
(링크를 바로 첨부하고 싶으나 파라미터를 보니 업데이트가 이뤄질 경우 제대로된 다운이 안될 것 같아서 직접 늘 받는게 좋을 것 같다.)


Gradle Dependency Add

해당 파일은 확장자가 .mmdb 인데 MaxMind DataBase 의 줄임말이 아닐까 싶은..? 여튼 파일을 읽기 위해서는 전용 라이브러리가 필요하며 다음을 추가한다.

// geoip2
implementation "com.maxmind.geoip2:geoip2:4.1.0"

버전은 maven repository 에서 알아서 최신을 쓰던 말던 하면 될 것 같다.


Config

아까 받은 GZIP 파일을 압출을 풀면 .mmdb 확장자를 가진 파일이 있을텐데 그걸 resource 폴더 내에 아무 곳에 위치 하자.
만약 배포 서버에서 항상 업데이트된 파일을 다운로드 받게끔 스크립트를 짜고 해당 파일을 다운로드 받아 정해진 위치에 있는 파일을 읽어오는 방식으로 한다면 yml 에서 경로를 지정해두고 해당 @Value 를 호출해서 쓰면 될 것 같으나 매번 업데이트 까진 귀찮아서 글쓰는 시점 파일을 호출해오는 것으로 일단 작성한다. 그래서 resource 위치 내부에 저장했다.

  • resource/geoIp/GeoLite2-Country.mmdb

그리고 해당 파일을 불러오는 Bean 을 추가한다.

import com.maxmind.db.CHMCache;
import com.maxmind.geoip2.DatabaseReader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import java.io.IOException;

@Configuration
public class GeoIpConfig {

    @Bean
    public DatabaseReader databaseReader() throws IOException {
        ClassPathResource classPathResource = new ClassPathResource("geoIp/GeoLite2-Country.mmdb");
        return new DatabaseReader
            .Builder(classPathResource.getInputStream())
            .withCache(new CHMCache())
            .build();
    }

}

HttpUtils

IP 를 추출하는 유틸 클래스를 하나 만든다. 기존에 사용하는게 있으면 추가 안해도 됨.

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

public class HttpUtils {

    private static final String[] IP_HEADER_CANDIDATES = {
        "X-Forwarded-For",
        "Proxy-Client-IP",
        "WL-Proxy-Client-IP",
        "HTTP_X_FORWARDED_FOR",
        "HTTP_X_FORWARDED",
        "HTTP_X_CLUSTER_CLIENT_IP",
        "HTTP_CLIENT_IP",
        "HTTP_FORWARDED_FOR",
        "HTTP_FORWARDED",
        "HTTP_VIA",
        "REMOTE_ADDR"
    };

    public static String getClientIpAddressIfServletRequestExist() {

        if (RequestContextHolder.getRequestAttributes() == null) {
            return "0.0.0.0";
        }

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        for (String header: IP_HEADER_CANDIDATES) {
            String ipList = request.getHeader(header);
            if (ipList != null && !ipList.isEmpty() && !"unknown".equalsIgnoreCase(ipList)) {
                return ipList.split(",")[0];
            }
        }

        return request.getRemoteAddr();
    }

}

Filter

Filter 를 이용하여 요청이 올 때마다 IP를 먼저 확인하기 위해 코드를 추가하여 Security Filter 에 등록한다.

import com.maxmind.geoip2.DatabaseReader;
import com.maxmind.geoip2.exception.GeoIp2Exception;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.InetAddress;

@Slf4j
@Component
@RequiredArgsConstructor
public class IpAuthenticationFilter implements Filter {

    private final DatabaseReader databaseReader;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String ipAddress = HttpUtils.getClientIpAddressIfServletRequestExist();
        InetAddress inetAddress = InetAddress.getByName(ipAddress);
        String country;

        if (!ipAddress.equals("127.0.0.1") && !ipAddress.equals("0:0:0:0:0:0:0:1") && !ipAddress.startsWith("192.") && !ipAddress.startsWith("172.") && !ipAddress.startsWith("10.")) {
            try {
                country = databaseReader.country(inetAddress).getCountry().getName();
                if (country == null || (!country.equals("South Korea"))) {
                    log.info("Access Rejected : {}, {}", ipAddress, country);
                    return;
                }
            } catch (GeoIp2Exception e) {
                e.printStackTrace();
                throw new UnauthorizedException("Access Rejected : " + ipAddress);
            }
        }

        chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) {
        log.info("IP Authentication Filter Init..");
    }

    @Override
    public void destroy() {
        log.info("IP Authentication Filter Destroy..");
    }
}

아래는 SecurityConfig 쪽에 filter 를 추가한 코드

http.addFilterBefore(ipAuthenticationFilter, JwtAuthenticationFilter.class);

Ip 대역을 체크하는 filter 에서 조건에 사설 IP 대역이 추가되어있는데 클라우드 환경에서 배포 후에 접근하는 Ip 가 사설 Ip 로 확인이 되어서 조건을 추가했다. 해당되지 않다면 로컬만 체크하면 될 것 같다.
그리고 country 에 저장된 값이 South Korea 가 혹시나 아니라면 debug 를 통해 정확히 확인하는것이 좋다. 예를 들어 KR 이라던가..


Test

해외 지역으로 설정하여 사이트를 테스트해주는 사이트가 있는데 여기서 체크하려는 도메인을 확인했을 때 로그가 Access Rejected 라고 나오면 성공이다.
Pingdom <- 체크할 사이트 링크

2024-06-25 18:40:03.909  INFO 1 --- [nio-8080-exec-8] c.k.d.i.c.geoIp.IpAuthenticationFilter   : Access Rejected : 15.228.173.115, Brazil

상파울로 설정하고 확인하니 로그가 위와 같이 남겨졌다.


Error Handling

  • DatabaseReader 를 build 할 때 ClassPathResource 를 사용하여 getFile() 로 처음에 넘겼는데 jar 배포환경에서는 파일 호출이 불가능하다. 그래서 getInputStream() 으로 변경하여 처리함. 메소드가 없는 줄 알았는데 있었음.

Source

반응형