Published on

Whois API 와 DRF Permission 로 해외 IP 차단하기

Authors

해외 IP 차단은 공공데이터에서 제공하는 Whois API 를 이용해서 구현이 가능합니다.

특정 API 요청을 보내는 IP 가 어떤 국가의 IP 인지 알아내고, 해외라면 차단(Block) 시키면 됩니다.

차단방식은 권한에 대한 에러로 처리할 수 있도록 DRF의 Permission 클래스를 상속받아서 구현했습니다.

1. IP 가져오기

먼저 DRF로 들어온 API 요청에 대해 요청한 IP를 추출 해야합니다.

request객체의 header에서 IP 를 가져올 수 있습니다.


def get_client_ip(request):
    x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
    if x_forwarded_for:
        ip = x_forwarded_for.split(",")[0]
    else:
        ip = request.META.get("REMOTE_ADDR")
    return ip

  • HTTP_X_FORWARDED_FOR는 클라이언트의 원래 IP 주소를 포함합니다. 프록시 서버나 로드 밸런서를 통해 요청이 전달될 때 사용됩니다.

    X-Forwarded-For: client, proxy1, proxy2

  • REMOTE_ADDR 은 요청을 보낸 클라이언트의 IP 주소를 나타냅니다. 프록시 서버를 사용하지 않을 때 일반적으로 사용됩니다.

헤더값을 사용하는 예시 Django5.0버전의 문서https://docs.djangoproject.com/ko/5.0/ref/request-response/

2. Whois API

해당 IP 가 어떤 국가인지 Whois API로 조회 합니다.

아래 링크에서 API를 사용할 수 있도록 신청하고 키를 발급받습니다.

https://www.data.go.kr/data/15094277/openapi.do

2.1 신청 방법

활용 신청 을 눌러 Whois API를 신청 할 수 있습니다.

해외IP차단_1
활용목적은 간단하게 입력해도 무관합니다. 해외IP차단_2

2.2 IP에 대한 국가 정보 요청 코드


def get_ip_country(ip_address: str):
    whois_secrets = get_secret("whois")
    whois_secret_key = whois_secrets.get("key") # whois api key 입니다
    url = "http://apis.data.go.kr/B551505/whois/ip_address"
    params = {
        "serviceKey": whois_secret_key,
        "query": ip_address,
        "answer": "json",
    }
    response = requests.get(url, params=params)
    country_code = handle_api_response(response) # 성공 or 실패 처리 생략
    return country_code

API 엔드포인트: http://apis.data.go.kr/B551505/whois/ip_address

serviceKey 가 API 키 이며, 일반 인증키(Decoding) 값으로 했을때 성공 응답이 수신되었습니다.

해외IP차단_3

2.3 Whois API Error code 참고

해외IP차단_4
에러코드에러메세지설명
1APPLICATION ERROR어플리케이션 에러
4HTTP_ERRORHTTP 에러
12NO_OPENAPI_SERVICE_ERROR해당 오픈 API 서비스가 없거나 폐기됨
20SERVICE_ACCESS_DENIED_ERROR서비스 접근거부
22LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR서비스 요청제한횟수 초과에러
30SERVICE_KEY_IS_NOT_REGISTERED_ERROR등록되지 않은 서비스키
31DEADLINE_HAS_EXPIRED_ERROR활용기간 만료
32UNREGISTERED_IP_ERROR등록되지 않은 IP
99UNKNOWN_ERROR기타에러

3. 차단 로직

3.1 permission class

기본 restframe work 의 BasePermission 을 상속받은 새로운 클래스 입니다.

permission 과정에서 request, user 객체를 통해 ip 와 그에 대한 검증까지 수행이 가능합니다.

[bool(user), user.is_authenticated] 는 원래 has_permission 의 기본 리턴 값과 동일하고, validate_ip_address() 함수를 추가해 이 단계에서 Cutom Error(403) 를 발생시키는 것이 좋다고 생각했습니다.

from rest_framework.permissions import BasePermission

from app.common.utils import get_client_ip
from app.common.validators import validate_ip_address

class CommonPermission(BasePermission):
    def has_permission(self, request, view):
        user = request.user
        conditions = [bool(user), user.is_authenticated]
        ip_address = get_client_ip(request)
        conditions += [validate_ip_address(ip_address, user)]
        return all(conditions)

3.2 IP 제한 로직

Whois API를 통해서 한번 IP 에 대한 국가를 알아냈다면, 다시 요청을 보낼 필요가 없으니 DB에 저장해두고 사용 할 수 있습니다.

유저-차단(희망)여부에 따라서 OverseaAccessError를 내보낼지 결정할 수 있습니다.

from typing import Optional, Tuple

def get_country_code(ip_address: str) -> str:
    try:
        return get_ip_country(ip_address)
    except Exception as e:
        raise ValidationError(f"Whois API error: {e}")

def validate_ip_address(ip_address: str, user_obj: Optional[str] = None) -> str:

    PASS = "PASS"
    SUCCESS = "SUCCESS"

    # 로컬에서 요청하는 경우 등은 PASS
    if not user_obj or ip_address == "127.0.0.1":
        return PASS

    if not ip_address:
        raise ValidationError("IP address가 존재하지 않습니다.")

    # IP 와 국가 저장
    ip_country_obj, created = IpCountry.objects.get_or_create(ip=ip_address)
    country_code = ip_country_obj.country if not created else ""

    if created:
        country_code = get_country_code(ip_address) # Whois API
        ip_country_obj.country = country_code
        ip_country_obj.save()

		# 유저의 IP 차단 설정여부
    if user_obj:
        disable_oversea_obj, _ = DisableOversea.objects.get_or_create(user=user_obj)

        if disable_oversea_obj.is_disabled and country_code != "KR":
            raise OverseaAccessError(detail=f"{ip_address} - {country_code}: 국내 IP가 아닙니다.")

    return SUCCESS

3.3 Cutom Error

403 error 규격에 맞게 APIException 클래스를 상속받아 해외 로그인 차단에러를 아래처럼 생성 할 수 있습니다.

from rest_framework import status
from rest_framework.exceptions import APIException

class OverseaAccessError(APIException):
    status_code = status.HTTP_403_FORBIDDEN
    default_detail = "해외 로그인 차단 에러"
    default_code = "forbidden"

hongreat 블로그의 글을 봐주셔서 감사합니다!