产品中不同客户端请求下的 IP 归属地分析方法

笔记哥 / 04-30 / 9点赞 / 0评论 / 485阅读
在产品中可能存在不同客户端,请求同一个服务端接口的场景。 例如小程序和App或者浏览器中,如果需要对请求的归属地进行分析,前提是需要先获取请求所在的国家或城市,这种定位通常需要主动授权,而用户一般是不愿意提供的,就需要通过请求的IP来进行归属地计算。 IP地址一般分为两种,IPV4和IPV6,相应的计算方式也有差异,以国家维度来参考,每个国家都有对应的网段范围,计算网段中的最小和最大IP地址的对应数值,然后对比请求的IP地址,来判断属于哪个国家的网段范围。 ```java import cn.hutool.core.net.Ipv4Util; import cn.hutool.core.util.StrUtil; import java.math.BigInteger; import java.net.InetAddress; public class IpCalculate { public static void main(String[] args) throws Exception { // IPv4 网段 String ipv4Network = "IPv4 网段"; String[] ipv4Param = StrUtil.splitToArray(ipv4Network, "/"); // IPv4 起始和结束IP String ipv4StartIp = Ipv4Util.getBeginIpStr(ipv4Param[0],Integer.parseInt(ipv4Param[1])); String ipv4OverIp = Ipv4Util.getEndIpStr(ipv4Param[0],Integer.parseInt(ipv4Param[1])); System.out.println(ipv4StartIp); System.out.println(ipv4OverIp); // IPv4 起始和结束IP对应的Long值 System.out.println(Ipv4Util.ipv4ToLong(ipv4StartIp)); System.out.println(Ipv4Util.ipv4ToLong(ipv4OverIp)); // IPv6 网段 String ipv6Network = "IPv6 网段"; String[] ipv6Param =ipv6Network.split("/"); int prefixLength = Integer.parseInt(ipv6Param[1]); // IPv6 起始和结束IP InetAddress baseAddress = InetAddress.getByName(ipv6Param[0]); BigInteger baseValue = new BigInteger(1, baseAddress.getAddress()); BigInteger mask = BigInteger.ONE.shiftLeft(128).subtract(BigInteger.ONE) .shiftRight(128 - prefixLength).shiftLeft(128 - prefixLength); BigInteger minIp = baseValue.and(mask); BigInteger maxIp = minIp.add(BigInteger.ONE.shiftLeft(128 - prefixLength).subtract(BigInteger.ONE)); System.out.println(toIPv6String(minIp)); System.out.println(toIPv6String(maxIp)); // IPv6 起始和结束IP对应的Long值 System.out.println(minIp); System.out.println(maxIp); } private static String toIPv6String(BigInteger value) throws Exception { byte[] bytes = value.toByteArray(); byte[] ipv6Bytes = new byte[16]; int start = bytes.length > 16 ? bytes.length - 16 : 0; int length = Math.min(bytes.length, 16); System.arraycopy(bytes, start, ipv6Bytes, 16 - length, length); return InetAddress.getByAddress(ipv6Bytes).getHostAddress(); } } ``` 不过网段地址和国家的对应关系需要进行维护,如果归属地分析不需要非常精准,可以直接使用开源的字典库,比如使用比较多的就是GeoIP2组件。 ```xml com.maxmind.geoip2groupId> geoip2 ``` 通过组件中提供的API加载相应的文件字典,然后传入IP地址进行归属地判断,这里要注意争议和敏感地区的处理,如果出错产品可不止是上热搜的问题了。 ```java import com.maxmind.geoip2.DatabaseReader; import java.io.File; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; public class GeoIpTool { public static void main(String[] args) throws Exception { // 读取IP库文件 File ipFile = new File("IP文件库"); DatabaseReader reader = new DatabaseReader.Builder(ipFile).build(); // IPV4地址 InetAddress ipV4 = InetAddress.getByName("IPV4"); if (ipV4 instanceof Inet4Address){ System.out.println(reader.country(ipV4)); System.out.println(reader.country(ipV4).getCountry()); // 默认英文名 System.out.println(reader.country(ipV4).getCountry().getName()); // 查询中文名 System.out.println(reader.country(ipV4).getCountry().getNames().get("zh-CN")); } // IPV6地址 InetAddress ipV6 = InetAddress.getByName("IPV6"); if (ipV6 instanceof Inet6Address){ System.out.println(reader.country(ipV6)); System.out.println(reader.country(ipV6).getCountry()); // 默认英文名 System.out.println(reader.country(ipV6).getCountry().getName()); // 查询中文名 System.out.println(reader.country(ipV6).getCountry().getNames().get("zh-CN")); } } } ``` 如果需要非常精确的实时归属地分析,可以购买专业的IP网段数据,实时更新到本地的数据库中,作为IP字典使用,获取请求的IP后,直接范围匹配即可。 ```sql CREATE TABLE `ip_place` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `network` varchar(100) DEFAULT NULL COMMENT '网段区间', `min_ip` bigint(20) DEFAULT NULL COMMENT '最小IP', `max_ip` bigint(20) DEFAULT NULL COMMENT '最大IP', `min_ip_number` bigint(20) DEFAULT NULL COMMENT '最小IP数值', `max_ip_number` bigint(20) DEFAULT NULL COMMENT '最大IP数值', `ip_place` varchar(100) DEFAULT NULL COMMENT '归属地', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='IP归属地'; ``` 最后需要补充说一句,对于很多标准的数据,尽可能在项目最初就设计好字典枚举或者数据表,避免后续规范时面临数据清洗的问题。