[Spring] GeoIP를 이용한 해외 IP 차단Back-End/Spring2025. 1. 16. 21:24
Table of Contents
MaxMind 에서 데이터베이스 다운로드
MaxMind에서 먼저 데이터베이스를 받아와야한다.
국가를 제외한 나머지 자료(시, 도 등)는 꽤 부정확하다는 글이 많고, 해외인지 아닌지가 가장 중요하기 때문에 country 데이터베이스만 사용하기로 했다.
아래 사이트에서 회원 가입을 한 후 country 데이터베이스를 다운로드 받는다.(GeoLite2-Country.mmdb)
https://www.maxmind.com/en/home
스프링에 적용
의존성 추가
build.gradle에 geoip 의존성을 추가해준다.
implementation "com.maxmind.geoip2:geoip2:4.1.0"
서비스 로직 작성
아래 서비스 계층에는
- getClientIP() : request 객체를 바탕으로 IP를 추출하는 메서드
- getCountryByIp(String ip) : IP를 바탕으로 영문 국가명을 반환하는 메서드
이렇게 두 개의 주요 메서드가 존재한다.
getCountryByIp는 같은 네트워크에 접속된 아이피는 국가명이 아닌 "내부 아이피" 문자열을 반환한다.
import com.maxmind.geoip2.DatabaseReader;
import com.maxmind.geoip2.model.CountryResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.io.File;
import java.net.InetAddress;
@Service
public class IpService
{
private final DatabaseReader dbReader;
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"
};
// IP 추출 메서드
public String getClientIP()
{
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();
}
public IpService(@Value("${geoip.database.path}") String databasePath)
{
try
{
File database = new File(databasePath);
this.dbReader = new DatabaseReader.Builder(database).build();
}
catch (Exception e)
{
throw new RuntimeException("GeoIP 데이터베이스 읽기 실패 : " + e.getMessage(), e);
}
}
// IP를 바탕으로 국가명을 반환하는 메서드
public String getCountryByIp(String ip)
{
if (isInternalIp(ip)) return "내부 아이피"; // 내부 IP 확인
try
{
InetAddress ipAddress = InetAddress.getByName(ip);
CountryResponse response = dbReader.country(ipAddress);
return response.getCountry().getName();
}
catch (Exception e)
{
return "Unknown Country";
}
}
private boolean isInternalIp(String ipAddress)
{
if ("127.0.0.1".equals(ipAddress)) return true; // IPv4 루프백
if ("0:0:0:0:0:0:0:1".equals(ipAddress)) return true; // IPv6 루프백
return ipAddress.startsWith("10.") || ipAddress.startsWith("192.168.") || (ipAddress.startsWith("172.") && isInRange(ipAddress)); // 사설 IP 범위 확인
}
// 두 번째 옥텟 범위 확인(16 ~ 31)
private boolean isInRange(String ipAddress)
{
try
{
String[] parts = ipAddress.split("\\.");
int secondOctet = Integer.parseInt(parts[1]);
return secondOctet >= 16 && secondOctet <= 31;
}
catch (Exception e)
{
return false;
}
}
}
application.yml에 geoip 데이터베이스 위치 설정
geoip:
database:
path: /Users/.../GeoLite2-Country.mmdb
데이터베이스(.mmdb)가 위치한 경로를 설정해준다.
이렇게 해두면 @Value 애노테이션에서 DB의 위치를 인식한다.
서블릿 필터에 등록
모든 해외 요청에 대해서 IP를 기반으로 차단하려면 서블릿 필터에 등록해야 한다.
import com.seungwook.main.service.IpService;
import com.seungwook.main.service.LoginFailureListService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Slf4j
@Component
@RequiredArgsConstructor
public class IpBanFilter extends OncePerRequestFilter
{
private final IpService ipService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException
{
String ip = ipService.getClientIP();
String country = ipService.getCountryByIp(ip);
String requestURI = request.getRequestURI(); // 요청 경로
String method = request.getMethod(); // 요청 메서드
if(country.equals("내부 아이피")) // 내부 아이피라면 다음 필터로 전달
{
filterChain.doFilter(request, response);
return;
}
if(!country.equals("South Korea")) // 아이피가 한국이 아니라면
{
log.info("Access Rejected(Foreign IP) {}({}) / ({} -> {})", ip, country, method, requestURI);
response.sendError(HttpServletResponse.SC_FORBIDDEN); // http 403 상태코드 전달
return;
}
log.info("{}({}) / ({} -> {})", ip, country, method, requestURI);
filterChain.doFilter(request, response); // 차단되지 않았다면, 다음 필터로 요청 전달
}
//정적 자원 요청은 필터링 제외
@Override
protected boolean shouldNotFilter(HttpServletRequest request)
{
String path = request.getRequestURI();
return path.startsWith("/css/") || path.startsWith("/js/") || path.startsWith("/images/") || path.startsWith("/favicon.ico");
}
}
이렇게 설정하면
2025-01-16 21:18:49.905 [http-nio-10000-exec-4] INFO com.seungwook.main.config.IpBanFilter - Access Rejected(Foreign IP) 54.83.136.80(United States) / (GET -> /)
2025-01-16 21:19:28.027 [http-nio-10000-exec-7] INFO com.seungwook.main.config.IpBanFilter - Access Rejected(Foreign IP) 54.248.152.138(Japan) / (GET -> /login)
위와 같은 로그가 남으면서 해외 접근이 차단된다.
실제로 VPN을 이용해서 서버에 접근했지만 모두 차단되었다.
레퍼런스
'Back-End > Spring' 카테고리의 다른 글
[Spring DB] 트랜잭션 전파 활용 (0) | 2024.04.11 |
---|---|
[Spring DB] 트랜잭션 전파 (1) | 2024.04.10 |
[Spring DB] 스프링 트랜잭션의 이해 (0) | 2024.04.06 |
[Spring DB] MyBatis (0) | 2024.04.03 |
[Spring DB] 데이터 접근 계층 테스트 (0) | 2024.04.02 |