java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot3 ip2region离线IP查询

SpringBoot3集成ip2region实现离线IP查询方案

作者:码路星河

做后端开发,IP归属地查询绝对是高频刚需场景:用户访问日志埋点、地域权限风控、用户地域画像、站点访问统计,几乎处处都能用得上,本文给大家介绍了如何基于ip2region搭建的离线IP查询的方案,需要的朋友可以参考下

引言

做后端开发,IP归属地查询绝对是高频刚需场景:用户访问日志埋点、地域权限风控、用户地域画像、站点访问统计,几乎处处都能用得上。之前项目里用过不少第三方在线IP查询接口,踩坑无数——高峰期接口限流、服务不稳定宕机,还存在用户IP隐私合规风险,后来改用ip2region这款纯离线开源查询方案,直接解决了所有痛点。

对比下来,基于ip2region搭建的离线IP查询方案,才更适配生产场景,全程本地运行、零外网依赖,单IP查询速度达微秒级,常规场景精准度完全够用,而且免费开源、无调用限制。这篇文章全程只讲实操、不堆砌理论,代码复制即可运行,从环境配置到生产避坑一站式讲透,直接照搬就能用到正式项目里。

一、项目环境与核心资源

先同步实操环境版本,避开版本兼容坑,本文全程基于ip2region 3.3.6最新稳定版开发,适配SpringBoot3生态:

核心资源说明:ip2region 官方拆分了IPv4和IPv6两套独立离线数据库,需分别下载对应xdb文件,这是实现双协议查询的关键,旧版单文件无法同时支持两种IP,必须下载官方最新版双库文件,避免数据不匹配、查询失败问题。

ip2region双库文件下载地址(Gitee官方): 1. IPv4专用库:https://gitee.com/lionsoul/ip2region/blob/master/data/ip2region_v4.xdb 2. IPv6专用库:https://gitee.com/lionsoul/ip2region/blob/master/data/ip2region_v6.xdb 下载后不要重命名、不要修改后缀,严格保留原名区分双库,贴合ip2region官方加载规范。

二、Maven依赖引入

打开项目 pom.xml,直接引入ip2region核心依赖,SpringBoot3 无需额外适配包,极简引入即可,额外推荐引入hutool工具包,简化后续IP获取和字符串处理,生产项目必备:

<!-- ip2region 离线IP归属地查询核心依赖 最新3.3.6版本 -->
<dependency>
    <groupId>org.lionsoul</groupId>
    <artifactId>ip2region</artifactId>
    <version>3.3.6</version>
</dependency>
<!-- 可选:hutool工具包,简化IP获取和字符串处理,生产常用 -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.25</version>
</dependency>

依赖刷新完成后,直接进入下一步数据库文件配置,全程贴合ip2region官方API写法,无需额外编写复杂配置类,上手即用。

三、xdb数据库文件放置

这一步很关键,ip2region双库文件必须规范放置,避免加载冲突,亲测最优路径方案:

  1. 在项目 resources 目录下,直接新建文件夹 ipdb(无需额外建子目录)
  2. 将下载好的 ip2region_v4.xdb(IPv4库)、ip2region_v6.xdb(IPv6库),直接放入 resources/ipdb 目录下
  3. 严格保留文件原名,不重命名、不修改后缀,防止ip2region加载路径匹配失败

文件放置完成后,目录结构参考下图:

重点避坑:打包时如果出现 xdb 文件丢失、损坏,需要在 pom.xml 的 build 配置中,禁止压缩 xdb 后缀文件,后面避坑章节会详细讲,双库文件都需要生效该配置。

四、封装通用IP查询工具类

ip2region 官方针对IPv4、IPv6双库做了统一API封装,不用手动维护两个独立Searcher,直接通过Config分别配置双库、再由Ip2Region统一托管即可,使用更简洁、稳定性更强。我采用单例模式,项目启动时一次性加载双库文件,避免重复IO占用内存,服务关闭时统一释放资源,完全适配SpringBoot3资源加载规范,全局可直接调用,代码复制即可落地:

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.lionsoul.ip2region.service.Config;
import org.lionsoul.ip2region.service.Ip2Region;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import java.io.InputStream;
/**
 * 离线IP归属地查询工具类
 * 遵循官方双库规范:IPv4+IPv6分开配置,统一托管查询
 * 完整覆盖所有内网IP段判断,杜绝公网查询误判
 * 单例模式,全局复用,避免重复加载数据库
 */
@Slf4j
@Component
public class IpRegionUtil {
    /**
     * IPv4、IPv6双库文件路径,对应resources/ipdb目录
     * 严格对应ip2region官方原文件名,不修改、不重命名
     */
    private static final String IPV4_XDB_PATH = "ipdb/ip2region_v4.xdb";
    private static final String IPV6_XDB_PATH = "ipdb/ip2region_v6.xdb";
    /**
     * ip2region官方统一查询对象,托管双库,无需手动拆分Searcher
     */
    private Ip2Region ip2Region;
    /**
     * 项目启动初始化,加载ip2region双库文件
     * 采用BufferCache缓存策略,兼顾查询速度与内存占用
     */
    @PostConstruct
    public void init() {
        try {
            // 加载IPv4数据库配置
            ClassPathResource v4Resource = new ClassPathResource(IPV4_XDB_PATH);
            InputStream v4Is = v4Resource.getInputStream();
            Config v4Config = Config.custom()
                    .setCachePolicy(Config.BufferCache)
                    .setSearchers(10)
                    .setXdbInputStream(v4Is)
                    .asV4();
            // 加载IPv6数据库配置
            ClassPathResource v6Resource = new ClassPathResource(IPV6_XDB_PATH);
            InputStream v6Is = v6Resource.getInputStream();
            Config v6Config = Config.custom()
                    .setCachePolicy(Config.BufferCache)
                    .setSearchers(10)
                    .setXdbInputStream(v6Is)
                    .asV6();
            // 统一创建ip2region查询对象
            ip2Region = Ip2Region.create(v4Config, v6Config);
            log.info("ip2region初始化成功,IPv4+IPv6双库加载完成");
        } catch (Exception e) {
            log.error("ip2region初始化失败,请检查双库文件路径和完整性", e);
            throw new RuntimeException("ip2region异常,无法提供IP查询服务");
        }
    }
    /**
     * 统一IP查询入口,自动识别IPv4/IPv6
     * 完整过滤所有内网IP,包含10段、172段、192段、本地回环
     */
    public String getIpRegion(String ip) {
        try {
            // 空IP直接返回
            if (ip == null || ip.isBlank()) {
                return "内网IP|内网IP";
            }
            // 判断是否为内网IP,包含IPv4全内网段 + IPv6回环
            if (isPrivateIp(ip)) {
                return "内网IP|内网IP";
            }
            // 调用ip2region执行公网IP查询
            return ip2Region.search(ip);
        } catch (Exception e) {
            log.error("ip2region IP查询异常,IP:{}", ip, e);
            return "查询失败";
        }
    }
    /**
     * 完整内网IP判断逻辑
     * IPv4内网范围:
     * 1. 127.0.0.1 本地回环
     * 2. 10.0.0.0 ~ 10.255.255.255
     * 3. 172.16.0.0 ~ 172.31.255.255
     * 4. 192.168.0.0 ~ 192.168.255.255
     * IPv6内网:::1 / 0:0:0:0:0:0:0:1 回环地址
     */
    private boolean isPrivateIp(String ip) {
        // IPv4内网判断
        if (ip.contains(".")) {
            // 本地回环
            if ("127.0.0.1".equals(ip)) {
                return true;
            }
            // 192.168网段
            if (ip.startsWith("192.168.")) {
                return true;
            }
            // 10网段
            if (ip.startsWith("10.")) {
                return true;
            }
            // 172.16.0.0 ~ 172.31.255.255 内网段
            if (ip.startsWith("172.")) {
                String[] parts = ip.split("\\.");
                if (parts.length == 4) {
                    try {
                        int secondSegment = Integer.parseInt(parts[1]);
                        return secondSegment >= 16 && secondSegment <= 31;
                    } catch (NumberFormatException e) {
                        return false;
                    }
                }
            }
            return false;
        }
        // IPv6回环地址判断
        return "::1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip);
    }
    /**
     * 服务关闭,释放ip2region资源
     */
    @PreDestroy
    public void close() {
        try {
            if (ip2Region != null) {
                ip2Region.close();
                log.info("ip2region资源释放完成");
            }
        } catch (Exception e) {
            log.error("ip2region资源释放失败", e);
        }
    }
}

工具类核心说明(贴合ip2region 3.3.6官方规范):

五、编写测试接口,快速验证双协议功能

写一个极简的Controller测试接口,支持两种查询模式:手动传入指定IP查询(兼容IPv4+IPv6)、自动获取当前请求IP查询,适配本地调试、Nginx代理、IPv6部署场景,基于ip2region快速验证功能是否正常:

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@Slf4j
@RestController
@RequestMapping("/ip")
@RequiredArgsConstructor
public class IpController {
    private final IpRegionUtil ipRegionUtil;
    /**
     * 手动传入IP查询归属地
     * @param ip 目标IP
     * @return 归属地结果
     */
    @GetMapping("/query")
    public String queryIp(@RequestParam String ip) {
        return ipRegionUtil.getIpRegion(ip);
    }
    /**
     * 获取当前请求的真实IP并查询归属地
     * 适配Nginx、反向代理场景,解决内网IP获取问题
     */
    @GetMapping("/current")
    public String getCurrentIpRegion(HttpServletRequest request) {
        // 优先获取代理转发的真实客户端IP
        String realIp = request.getHeader("X-Real-IP");
        if (realIp == null || realIp.isBlank()) {
            realIp = request.getRemoteAddr();
        }
        log.info("当前请求真实客户端IP:{}", realIp);
        return ipRegionUtil.getIpRegion(realIp);
    }
}

六、启动项目,接口测试

1. 启动SpringBoot项目,观察控制台日志,出现 ip2region初始化成功,IPv4+IPv6双库加载完成 字样,说明双库文件加载正常,核心工具初始化完成

2. 打开Postman或者浏览器,调用测试接口:

IPv4正常返回结果示例:中国|江苏省|南京市|0|CN

IPv6正常返回结果示例:中国|北京|北京市|0|CN

各类内网IP(10段、172段、192段、本地回环)测试结果:内网IP|内网IP,过滤逻辑完全生效

七、常见坑与解决方案

1. 打包后找不到xdb文件,项目启动失败

原因:Maven打包时,默认压缩资源文件,导致ip2region依赖的xdb文件损坏;或者双库文件路径写错、少放其中一个库文件。

解决方案:在 pom.xml 的 build 节点中,添加以下配置,禁止压缩 xdb 后缀文件:

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <excludes>
                <exclude>*.xdb</exclude>
            </excludes>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>*.xdb</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>
</build>

2. 反向代理后,获取不到真实客户端IP

生产项目基本都用Nginx代理,直接用request.getRemoteAddr()获取的是服务器内网IP,需要Nginx配置转发真实IP,确保ip2region能查询到真实用户IP:

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

Java代码中优先读取 X-Real-IP 请求头,和我上面Controller代码一致。

八、生产进阶用法

九、总结

SpringBoot3集成ip2region实现离线IP查询,全程不到15分钟就能落地,纯本地运行、零外网依赖、严格遵循官方双库规范支持IPv4+IPv6、查询性能拉满,完全能满足中小项目到生产级别的IP查询需求,不管是传统IPv4网络,还是新部署的IPv6环境,都能稳定适配。整个过程没有复杂配置,工具类和接口代码直接复用即可,文中的避坑方案都是实战踩坑总结,照着操作基本不会出现异常。

对比第三方在线接口,ip2region离线方案更稳定、更安全,还没有调用次数限制,也不用担心隐私合规风险,强烈建议大家把项目里的在线IP接口替换成这套方案,上手成本极低,实用性拉满!

以上就是SpringBoot3集成ip2region实现离线IP查询方案的详细内容,更多关于SpringBoot3 ip2region离线IP查询的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文