Appearance
Java 怎么获取用户 IP 归属地?在线接口版工具类(Spring Boot 可用)
先给结论:Java 拿 IP 归属地,最省事的是调在线 HTTP 接口:一个 GET + Jackson 解析,几十行封装成工具类,不依赖任何本地库文件。 淘宝老接口早废了,ip2region 离线库要自己下 xdb 文件还得惦记更新,中小项目为查个省市维护一套离线库,不划算。下面是能直接抄的实现。
一、老办法为什么越来越不好用
翻以前的博客,教 Java 查归属地的方案就三类,各有各的坑:
- 淘宝 IP 接口(
ip.taobao.com):多年前已停止服务,老教程还在抄,照着写必挂; - 纯真/ip2region 离线库:要下数据文件、引 SDK,还得自己盯着更新——IP 段一变结果就旧;
- 自建 IP 库:从 APNIC 拉分配数据自己解析,只能到机构级,省市都费劲。
在线接口没这些包袱:更新是服务商的事,你只管调。
二、方案设计
三步走:
- 拿真实 IP:后端在 Nginx 后面时,
request.getRemoteAddr()拿到的是 Nginx 的地址,得从X-Forwarded-For里取客户端 IP(前提:Nginx 配了proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for); - 查归属地:
GET https://ip9.com.cn/get?ip=<ip>,返回 JSON:country/prov/city/isp/lng/lat等字段,免费版支持 IPv4/IPv6、无需注册; - 缓存 + 降级:同一 IP 一天内反复查没意义,本地缓存 24 小时;接口超时返回 null,调用方记“未知”继续走流程,别让归属地查询拖垮主流程。
三、Java 实现
依赖只需要 Jackson(解析 JSON)。JDK 11+ 自带的 java.net.http.HttpClient 发请求,不用引 HTTP 库:
java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
public class IpLocation {
private static final String API = "https://ip9.com.cn/get?ip=";
private static final long TTL_MS = 24 * 3600 * 1000L; // 缓存24小时
private final HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2)).build();
private final ObjectMapper mapper = new ObjectMapper();
private final ConcurrentHashMap<String, CacheItem> cache = new ConcurrentHashMap<>();
public record Location(String ip, String country, String prov,
String city, String isp, String lng, String lat) {}
private record CacheItem(Location loc, long expireAt) {}
/** 查归属地:先走缓存,miss 再调接口 */
public Location lookup(String ip) {
CacheItem hit = cache.get(ip);
if (hit != null && hit.expireAt() > System.currentTimeMillis()) {
return hit.loc();
}
Location loc = remote(ip);
if (loc != null) {
cache.put(ip, new CacheItem(loc, System.currentTimeMillis() + TTL_MS));
}
return loc;
}
private Location remote(String ip) {
try {
HttpRequest req = HttpRequest.newBuilder(URI.create(API + ip))
.timeout(Duration.ofSeconds(3))
.header("User-Agent", "ip-location/1.0")
.GET().build();
HttpResponse<String> resp = http.send(req,
HttpResponse.BodyHandlers.ofString());
JsonNode root = mapper.readTree(resp.body());
if (root.path("ret").asInt() != 200) {
return null; // 非法 IP 返回 ret=400,按查不到处理
}
JsonNode d = root.path("data");
return new Location(
d.path("ip").asText(), d.path("country").asText(),
d.path("prov").asText(), d.path("city").asText(),
d.path("isp").asText(), d.path("lng").asText(),
d.path("lat").asText());
} catch (Exception e) {
return null; // 超时/网络异常:降级,不让归属地查询拖垮主流程
}
}
/** 从 X-Forwarded-For 取第一个公网 IP(仅信任自家 Nginx 设置的头) */
public static String resolveClientIp(jakarta.servlet.http.HttpServletRequest req) {
String xff = req.getHeader("X-Forwarded-For");
if (xff != null && !xff.isBlank()) {
for (String part : xff.split(",")) {
String ip = part.trim();
if (!ip.isEmpty() && !isPrivate(ip)) {
return ip;
}
}
}
return req.getRemoteAddr();
}
private static boolean isPrivate(String ip) {
if (ip.startsWith("10.") || ip.startsWith("192.168.")
|| ip.startsWith("127.")) {
return true;
}
if (ip.startsWith("172.")) { // 172.16.0.0 - 172.31.255.255
try {
int second = Integer.parseInt(ip.split("\\.")[1]);
return second >= 16 && second <= 31;
} catch (Exception e) {
return false;
}
}
return false;
}
}Spring Boot 里这么用——登录成功把来源地写进审计日志:
java
@PostMapping("/login")
public Result login(HttpServletRequest request, @RequestBody LoginForm form) {
String ip = IpLocation.resolveClientIp(request);
IpLocation.Location loc = ipLocation.lookup(ip);
auditLogService.save(form.getUserId(), ip,
loc == null ? "未知" : loc.prov() + " " + loc.city(),
loc == null ? "" : loc.isp());
// 继续正常登录流程...
return Result.ok();
}四、落地注意事项
- XFF 不能盲信:
X-Forwarded-For客户端可自己伪造,只在请求确认经过自家 Nginx/网关且网关覆盖了该头时才可信;直接暴露公网的接口别用上面的解析方法。 - 海外 IP 字段会空:查
8.8.8.8只返回国家和运营商,prov/city为空,展示时做空值处理(上面 record 已用asText()兜底)。 - 缓存设过期:运营商调整 IP 段后归属地会变,缓存 24 小时是折中;要实时就缩短 TTL,调用量也会上去。
- 版本差异:代码里的
jakarta.servlet.*是 Spring Boot 3;还在用 Boot 2 的换成javax.servlet.*。 - 识别机房要 VIP:免费接口的
isp只能看个大概;ip_type(ISP/BUS/IDC)是 VIP 字段,有需求再上。
总结
Java 项目接 IP 归属地,在线接口性价比最高:无鉴权、免维护、字段够用。把 IpLocation 类扔进项目,注册成 Spring Bean 就能用——写审计日志、做地域统计、风控前置都够了。接口文档和字段说明见官网 https://www.ip9.com.cn,浏览器直接打开 https://ip9.com.cn/get?ip=114.114.114.114 就能看返回。先跑通再优化,别一上来就上离线库。