- Published on
Whois API 와 DRF Permission 로 해외 IP 차단하기
- Authors
- Name
- hongreat
- ✉️hongreat95@gmail.com
해외 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를 신청 할 수 있습니다.
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) 값으로 했을때 성공 응답이 수신되었습니다.
2.3 Whois API Error code 참고
에러코드 | 에러메세지 | 설명 |
---|---|---|
1 | APPLICATION ERROR | 어플리케이션 에러 |
4 | HTTP_ERROR | HTTP 에러 |
12 | NO_OPENAPI_SERVICE_ERROR | 해당 오픈 API 서비스가 없거나 폐기됨 |
20 | SERVICE_ACCESS_DENIED_ERROR | 서비스 접근거부 |
22 | LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR | 서비스 요청제한횟수 초과에러 |
30 | SERVICE_KEY_IS_NOT_REGISTERED_ERROR | 등록되지 않은 서비스키 |
31 | DEADLINE_HAS_EXPIRED_ERROR | 활용기간 만료 |
32 | UNREGISTERED_IP_ERROR | 등록되지 않은 IP |
99 | UNKNOWN_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"