[project] Client IP를 이용하여 위치 정보 반환하기

Eun Gi Ko·2023년 7월 11일
0

[ Petmo Project ]

Login에 성공한 사용자의 내동네 설정을 하기 위해 사용자의 위치 data가 필요 했다.

브라우저에 접속할때 발생하는 request의 header안에 있는 정보를 활용할 수 있지 않을까 생각하였다.

따라서 나는 WEB에 접속한 사용자의 네트워크 ip를 이용하여 사용자의 위도와 경도 data를 추출한 다음 이를 기반으로 지역data를 조회하기로 하였다.


설계 >

  1. Django META data를 이용해 client ip를 추출한다.
  2. Google geolocation API를 이용하여, 추출한 client ip로부터 위도와 경도 data를 추출한다.
  3. 위경도data를 KaKao Local Rest-API를 이용해 지역기반의 data를 얻는다.
  4. 전달 받은 data중에서 우리에게 필요한 data만 APIView로 표시한다.

try:
  client_ip_address  =
  				request.META.get('HTTP_X_FORWARDED_FOR') 
  					or request.META.get('REMOTE_ADDR')#현재 접속 ip
  print("client IP address: ", client_ip_address)
  if not client_ip_address:
      return Response({"error": "Could not get Client IP address."}, status=status.HTTP_400_BAD_REQUEST)

Django Server로 오는 HTTP요청의 Meta data중에 client IP 주소를 얻어오기 위하여 REMOTE_ADDR을 이용하여 사용자의 ip를 얻는다. 만약 사용자가 프록시등 우회하여 접속하는 경우를 대비하여 HTTP_X_FORWARDED_FOR(=XFF)을 이용해 실제 client IP를 추출한다.

어떤 client의 IP를 얻을려고 하는 경우, HTTP Header에는 XFF와 REMOTE_ADDR이 존재한다.

원래 실제 IP는 REMOTE_ADDR Header에 가지고 있게 되는데, 만약 사용자가 Proxy 서버를 이용하게 되면 정확한 client IP를 알 수 없게 된다.
이때 존재하는 것이 HTTP_X_FORWARDED_FOR(=XFF)이다. XFF는 Proxy 서버가 설정해주는 환경변수 이다. client + proxy 하게 되면 실제 client IP 정보를 담고있는 REMOTE_ADDR의 값이 Proxy 서버 IP로 설정되어 전송된다.

Proxy를 거치게 되면 X_FORWARDED_FOR에 실제 IP값에 Proxy IP가 실려 전송요청을 하게 되어 맨 첫번째 IP만을 받아 client IP로 인식해야 한다.

실제로 Log를 확인해본 결과, client IP address: [client가 접속한 network IP], [Proxy Server IP ] 이렇게 2개의 IP가 전송되어지고 있었다.

물론 해결 방법은 2개의 ip가 전송되어지는 경우 맨 처음의 ip값만을 받거나, 저장 시키는 방법이야 알고 있었고, 이 작업은 크게 어렵지 않은 작업이라고 생각 했다.


하지만 해결을 하는것도 중요하지만, 왜 발생한 거지? 라고 오류 원인을 잡아내는 것도 중요하다고 생각한다. (사실 오류는 원인을 알게되면 반은 해결한 것과 다름없다고 생각한다.)

"나는 Proxy Server를 사용하지 않았는데 왜 ProxyIP가 같이 전송되어지는 거지?"
라고 의문이 들었다. 

좀 더 찾아보니 웹서버나 WAS앞에 로드밸런서, Proxy 서버, Caching 서버가 존재하는 경우에 웹서버는 Proxy 서버나 장비IP에서 접속한 것으로 인식하게 된다고 한다.
따라서 web 서버는 Proxy 서버 IP를 실제 client IP로 인식하게 되고 웹 로그를 기록하게 된다고 한다.


Log에 찍힌 client IP를 보면 2개가 있는데 하나는 접속한 client의 network IP이고, 다른 하나(162.158.162.97)는 뭘까.. 찾아 보니 내가 서버를 베포할때 Region을 Singapore로 설정하였는데 설정한 서버 지역의 ip가 같이 들어오고 있었던 것이였다...!

당연히 싱가폴IP가 최후로 받은 data이니, 아무리 위도 경도 data를 Google GeoAPI로 추출하여 Kakao LocalAPI에 전송하여도 한국이 아닌 싱가폴 지역을 보내는데 당연히 data가 제대로 전송되어질 리가 없다..

나는 변조되지 않은 client IP가 필요하여 기존의code를 수정하였다.

X-Forwarded-For를 이용하면 콤마를 구분자로 Client 와 Proxy IP 가 들어가고 첫번째 IP를 가져와 client IP를 추출한다.

def get_clientIP(self, request):
        if request.META.get('HTTP_X_FORWARDED_FOR'):
            client_ip_address  = request.META.get('HTTP_X_FORWARDED_FOR').split(',')[0]
            print("use XFF, client IP address: ", client_ip_address)
        else:
            client_ip_address  = request.META.get('REMOTE_ADDR')
            print("use REMOTE, client IP address", client_ip_address)
        return client_ip_address

이렇게 코드를 수정하고 개발서버에서 다시 runserver를 하게 되면, 실제 client의 네트워크ip를 기반으로 위도, 경도가 잘 출력이 된다.

[추가]
배포한 서버의 상태에서 Log를 확인해 보면, client ip를 기준으로 위경도 data가 출력이 되어야 하는데 Proxy 서버IP 를 기준으로 위도 경도 data가 추출되어 진다... 이건 아직 왜 이렇게 되는지 더 찾아 봐야 겠지만, 현재 내가 예상하기로는 아마 Google API에서 client ip를 보내야 하는데 clientIP를 보내지 못하고 ServerIP를 전송하는 것 같아 제대로된 data가 전달 받지 못하고 있는거 같다.

따라서 어떤 data들이 지금 들어오고 있는지 확인할 필요가 있었다.

2개의 IP중 첫번째ip가 client_ip로 설정하게 한것은 해결이 되었지만, 서버에서 Google API로 요청할때 clientIP로 위도 경도를 요청하는 것이 아니라 서버의 ip로 위도 경도를 요청하고 있어서 제대로 출력이 돼지 않았던 것이였다.

그래서 data를 요청할때 client IP를 명시적으로 전달해 주면 되지 않을까? 찾아봤는데 googleGeolocation API에는 ip를 전달하는 필드 값이 없었다. 그래서 googleAPI를 버리고 다른 외부 api로 변경하였고 결과는 해결하였다.!

이 방법 말고도, fontend에서 clientIP를 기반으로 위도, 경도를 얻어서 backend로 전달하는 방법도 있었지만, frontend에서 client정보를 노출하고 싶지 않아서 가급적이면 backend에서 해결하고 싶었다..

최종으로 수정한 코드

class getIP(APIView):#ip기반 현위치 탐색
    permission_classes=[IsAuthenticated]#인가된 사용자만 허용
    
    def get_clientIP(self, request):
        x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
        if x_forwarded_for:
            print("ip주소들 ", x_forwarded_for)
            client_ip_address = x_forwarded_for.split(',')[0].strip()
            print("use XFF, client IP address: ", client_ip_address)
        else:
            client_ip_address  = request.META.get('REMOTE_ADDR')
            print("use REMOTE, client IP address", client_ip_address)
        return client_ip_address

    
    def get(self, request):
        try:
            client_ip_address = self.get_clientIP(request)  # 현재 접속 IP
            print("최종 client IP address:", client_ip_address)
            if not client_ip_address:
                return Response({"error": "Could not get Client IP address."}, status=status.HTTP_400_BAD_REQUEST)
            
            ip_geolocation_url=f'https://geo.ipify.org/api/v2/country,city?apiKey={IP_GEOAPI}&ipAddress={client_ip_address}'
            result=urlopen(ip_geolocation_url).read().decode('utf8')
            # print(urlopen(ip_geolocation_url).read().decode('utf8'))
        
            print("result: ", result)
            res_data=json.loads(result)
            # print("res_data", res_data)
            
            if not res_data:
                return Response({"error":"res_data is empty."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
            
        
            if res_data: #구글API에서 위도 경도를 추출하고 KAKAO_API에 전달
                print("client_ip_address", client_ip_address)
                location = res_data.get('location')
                Ylatitude = location.get('lat')#위도
                print("위도:",Ylatitude )
                Xlongitude = location.get('lng')#경도
                print("경도:, ",Xlongitude )

나의 전체 class 코드

class getIP(APIView):#ip기반 현위치 탐색
    permission_classes=[IsAuthenticated]#인가된 사용자만 허용
    
    def get_clientIP(self, request):
        x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
        if x_forwarded_for:
            print("ip주소들 ", x_forwarded_for)
            client_ip_address = x_forwarded_for.split(',')[0].strip()
            print("use XFF, client IP address: ", client_ip_address)
        else:
            client_ip_address  = request.META.get('REMOTE_ADDR')
            print("use REMOTE, client IP address", client_ip_address)
        return client_ip_address

    
    def get(self, request):
        try:
            client_ip_address = self.get_clientIP(request)  # 현재 접속 IP
            print("최종 client IP address:", client_ip_address)
            if not client_ip_address:
                return Response({"error": "Could not get Client IP address."}, status=status.HTTP_400_BAD_REQUEST)
            
            ip_geolocation_url=f'https://geo.ipify.org/api/v2/country,city?apiKey={IP_GEOAPI}&ipAddress={client_ip_address}'
            result=urlopen(ip_geolocation_url).read().decode('utf8')
            # print(urlopen(ip_geolocation_url).read().decode('utf8'))
        
            print("result: ", result)
            res_data=json.loads(result)#result 객체로 변환
            # print("res_data", res_data)
            
            if not res_data:
                return Response({"error":"res_data is empty."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
            
        
            if res_data: #구글API에서 위도 경도를 추출하고 KAKAO_API에 전달
                print("client_ip_address", client_ip_address)
                location = res_data.get('location')
                Ylatitude = location.get('lat')#위도
                print("위도:",Ylatitude )
                Xlongitude = location.get('lng')#경도
                print("경도:, ",Xlongitude )
                region_url= f'https://dapi.kakao.com/v2/local/geo/coord2regioncode.json?x={Xlongitude}&y={Ylatitude}'
                print("2:", region_url)#kakao url
                headers={'Authorization': f'KakaoAK {KAKAO_API_KEY}' }
                response=requests.get(region_url, headers=headers)
                
                datas=response.json().get('documents')
                print("datas: ", datas)
                if not datas:#추가
                    return Response({"error":"datas is empty."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
                response.json().get('error')#추가
                if response.status_code==200:
                    address=[]
                    for data in datas:
                        address.append({
                            'address_name': data['address_name'], 
                            'region_1depth_name': data['region_1depth_name'], 
                            'region_2depth_name': data['region_2depth_name'], 
                            'region_3depth_name': data['region_3depth_name'],
                        })
                    return Response(address, status=status.HTTP_200_OK)
                else:
                    return Response({"error":"Failed to get region data for IP address."}, status=status.HTTP_400_BAD_REQUEST)#error
            else:
                return Response({"error": "Failed to get geolocation data for IP address."}, status=status.HTTP_400_BAD_REQUEST)
        except Exception as e:
            return Response({"error": "Failed to Load open API data."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
   
   

배포 서버에서의 Log확인


성공 화면

profile
아악! 뜨거워!!

0개의 댓글