我是如何获取到前端用户的 IP,并根据 IP 来获取地理定位的

4,307 阅读6分钟

这是我参与更文挑战的第 5 天,活动详情查看:更文挑战

说在前面:今天出门团建,蛮爽的,景色也不错,就是有点累,没啥心思写文章。

微信图片_20210605230032.jpg

找到了一篇我在 2018 年原创的历史文章。虽然发布时间是早了点,但是他毕竟也是一篇原创文章……😂 原创证明不是必要的,主要是这篇文章首发不在掘金,而且是个老文章,有人已经将这篇文章搬运到其他网站了,因此认为有必要声明一下,防止最后统计时被认为是抄袭文章……

下面是原稿发布后台,估计这个就可以证明了吧……

image.png

就这样吧。


大家好,容我开场先讲个故事。 故事的经过是这样的:

有一天,产品同学突发奇想,他想获取到下单用户的地理位置分布,以便来统计用户群的分布,进而为后期的按地区精确推广活动来做准备。

  • 我:这个简单啊,下单的时候,给个地理定位的请求,来获取用户所在位置就行啊,这样的话还准确……
  • 产品兄:这个方案需要考虑下,如果要是请求用户的定位信息,首先呢,可能会引起用户的反感,毕竟咱们这个产品,从头到尾都没有用到过定位,突然啪的一下来个弹窗,用户肯定一是蒙逼,说你从头到尾都没有定位功能,要个定位权限是不是要搞事情;二是隐私意识强点,就直接拒绝掉了,这样直接就拿不到信息,无法达到预设的效果了。其次呢,咱们能少打扰用户,就少打扰用户,有句话说,悄悄地进村,打枪的不要。就是说呢,让用户流畅的使用下来整个流程,让用户用的爽,一直是咱们的原则……
  • 产品兄:我听说有个 IP 定位,这东西蛮爽的,也不打扰用户,再说 IP 这个东西,只要用户访问我们的服务,绝对有这个东西能拿到,我们就拿他这个东西来反查下,这样咱们要的省市信息就这样到手了,而且这样的成功几率高,不用担心用户拒绝定位导致数据统计不全……
  • 后端同学:IP 这个东西的确可以,但是呢,我们的服务都在负载均衡/反向代理服务后面,后端直接拿到的 IP 也是前端服务器的 IP,而不是用户直连我们前端服务器的 IP,所以这个东西难办啊……
  • 后端同学:想当初我们的项目还不是前后端分离的时候,的确是可以直接拿到 IP 的……
  • 另外的前端同学:嗯,我这边也了解一些,我之前看到有纯前端获取 IP 的方法:
//创建 RTCPeerConnection 接口
let conn = new RTCPeerConnection({
		iceServers: []
	}) 
let noop = function(){}
conn.onicecandidate = function(ice){
	if (ice.candidate){
		// 使用正则获取 ip
		let ip_regex = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/
		let ip_addr = ip_regex.exec(ice.candidate.candidate)[1];
		console.log(ip_addr)
		conn.onicecandidate = noop
	}
}

//随便创建一个叫狗的通道 (channel)
conn.createDataChannel('dog')

//创建一个 SDP 协议请求
conn.createOffer(conn.setLocalDescription.bind(conn), noop)

作者:Illgo
链接:https://www.zhihu.com/question/20675353/answer/335325619
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

之后用 WebRTC 获取 IP 等关键词找了几篇类似的文章。这个比较全面。

获取访问者内网 IP

根据类似的文章,亲自试验了下,有两个问题:

  1. 我们有微信站的业务,在微信环境下获取不到 IP。这个方法在 Chrome 和 Firefox 下面很好用。
  2. 获取的是用户的内网 IP,用拿到的 IP 去反查,得到的结论是局域网地址。

这还查个毛线的查,于是……这个方案被放弃了……

  • 我:在 Google 找了一些方案,大多数方案都是通过前端调取一个接口,接口返回地理定位等信息。和后端同学说的一致,只要用户发请求,服务器能直接拿到用户网络出口的 IP 信息,然后咱们就调接口查一下就可以了。关于后端接口拿不到 IP 的问题,微信支付这边有类似的解决方案。

备注:获取用户 IP 指引 - 微信支付开发者中心

  • 我:咱们服务器相当于在代理后面,只要找运维去配置下,应该就可以了。
  • 后端同学:这个可以考虑,稍后我这边和运维联系下,让他们那边配置下,把用户请求代理服务器获取到的 IP 传递给我这边,这样就可以了。 于是问题解决,大功告成~

开玩笑,要真是这样的话,这篇文章就不会出现了😀

实际情况是这样的:

  • 后端同学:如果去配置的话,因为我们的服务器上面不只是部署了一套项目,修改配置可能影响较大……
  • 后端同学:其次呢,前端这边有一个 .Net 站是配置过的,这个站是之前前后端不分离时使用的站,现在只是用来部署静态文件,但是依然有动态代码的能力,用这个站来获取更容易,现在也不需要配置……

总之呢,这个任务,就落在前端这边了。

如果是现在的我,大概可能会拒绝接这种任务……毕竟分工所在。

好,现在让我们整理一下手头的资源:

  • 一个能直接获取到 IP 的站
  • 一个能根据 IP 反查所在区域的接口

你问我为啥不使用现成的服务呢?

主要是可靠性的原因,如果是你用的外部服务,万一服务挂了,一是只能等到服务商修复,时效可能比较长,万一服务商跑路了,还得费劲的切换服务;二是可以甩锅😀

毕竟代码是微信提供的,服务器是公司自有的服务器……别笑,我当初还真是这么想的。

下面是服务端的实现。 这部分实现呢,主要包含两个部分:

  • 获取 IP
  • 反查 IP 所属区域

首先我们新建一个一般处理程序(.ashx)。 下面是代码清单:

        /// <summary>
        /// 获取客户端IP地址
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        public static string GetAddress(HttpRequest request)
        {
            //可以透过代理服务器
            string remoteAddr = request.ServerVariables["HTTP_X_FORWARDED_FOR"];
            if (string.IsNullOrEmpty(remoteAddr))
            {
                //没有代理服务器,如果有代理服务器获取的是代理服务器的IP
                remoteAddr = request.ServerVariables["REMOTE_ADDR"];
            }

            if (!string.IsNullOrEmpty(remoteAddr)
                && remoteAddr.IndexOf(",") != -1
                && remoteAddr.Trim().Length > 6)
                remoteAddr = remoteAddr.Split(',')[0].Trim();

            if (remoteAddr.Length > 15)
                remoteAddr = GetIP4Address();

            return remoteAddr;
        }

        
        public static string GetIP4Address()
        {
            string IP4Address = String.Empty;

            foreach (IPAddress IPA in Dns.GetHostAddresses(HttpContext.Current.Request.UserHostAddress))
            {
                if (IPA.AddressFamily.ToString() == "InterNetwork")
                {
                    IP4Address = IPA.ToString();
                    break;
                }
            }

            if (!string.IsNullOrEmpty(IP4Address))
            {
                return IP4Address;
            }

            foreach (IPAddress IPA in Dns.GetHostAddresses(Dns.GetHostName()))
            {
                if (IPA.AddressFamily.ToString() == "InterNetwork")
                {
                    IP4Address = IPA.ToString();
                    break;
                }
            }

            return IP4Address;
        }
        

然后呢,前端调用上面暴露出的接口,就获取到了所需要的地理信息。

前端请求方法比较通用,就是一个很简单的 Ajax 请求。

之后在下单时把获取到的省市信息传到下单接口里面,这样我们就实现了获取到前端用户的 IP,并根据 IP 来获取地理定位的需求。