通过IP计算分析归属地

在产品中可能存在不同客户端,请求同一个服务端接口的场景。

例如小程序和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 复制代码
<dependency>
  <groupId>com.maxmind.geoip2groupId>
  <artifactId>geoip2</artifactId>
</dependency>

通过组件中提供的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归属地';

最后需要补充说一句,对于很多标准的数据,尽可能在项目最初就设计好字典枚举或者数据表,避免后续规范时面临数据清洗的问题。

相关推荐
num_killer33 分钟前
小白的Langchain学习
java·python·学习·langchain
期待のcode1 小时前
Java虚拟机的运行模式
java·开发语言·jvm
程序员老徐1 小时前
Tomcat源码分析三(Tomcat请求源码分析)
java·tomcat
a程序小傲2 小时前
京东Java面试被问:动态规划的状态压缩和优化技巧
java·开发语言·mysql·算法·adb·postgresql·深度优先
仙俊红2 小时前
spring的IoC(控制反转)面试题
java·后端·spring
阿湯哥2 小时前
AgentScope Java 集成 Spring AI Alibaba Workflow 完整指南
java·人工智能·spring
小楼v2 小时前
说说常见的限流算法及如何使用Redisson实现多机限流
java·后端·redisson·限流算法
与遨游于天地2 小时前
NIO的三个组件解决三个问题
java·后端·nio
czlczl200209252 小时前
Guava Cache 原理与实战
java·后端·spring
yangminlei2 小时前
Spring 事务探秘:核心机制与应用场景解析
java·spring boot