Appearance
Android 怎么获取 IP 归属地?Kotlin 直接调免费接口显示当前网络位置(完整代码)
做客服 App 的时候有个需求:登录页显示一句「当前网络:广东广州 · 中国电信」,让用户知道自己在哪联网;后面又加了登录安全提示,检测到网络位置和常用地不符就提醒二次验证。结果一搜「android 获取ip归属地」,搜出来的全是 2016 年的老代码,接口还是淘宝那个早就停服的 ip.taobao.com。这篇把 2026 年还能用的做法写清楚:Kotlin 客户端直接调免费接口,一次请求同时拿到出口 IP 和归属地,不用注册、不用 key。
一、先搞清:手机上能拿到的是哪个 IP
Android 上做这个,第一步就踩坑。手机不管连 WiFi 还是走流量,拿到的都是内网 IP:WiFi 下是路由器分的 192.168.x.x,流量下是运营商内网(10.x、100.64.x 之类)。拿这个 IP 去查归属地,返回要么是空要么是乱码——因为归属地是出口公网 IP 的属性,内网地址根本不入库。
网上很多老教程让你用 WifiManager 拿 IP 再查,方向就错了,那是路由器分给手机的局域网地址。正确做法只有一条:让请求走到外网,让服务器告诉你「你从哪个公网 IP 出来的」。这一步通常要调一个「查我的 IP」的接口,拿到出口 IP 再调第二个接口查归属地。
好消息是 IP9 的免费接口把两步合成了一步:GET https://ip9.com.cn/get 不传参数,直接返回当前请求方的公网 IP 和它的归属地,一个请求全齐。省掉「先查出口 IP 再查归属地」的两次往返,也省掉自己解析两次 JSON 的麻烦。
还有一点要提前说:手机流量和 WiFi 的出口不一样,流量走运营商基站出口,WiFi 走宽带出口,所以同一个手机在两个网络下查出来的归属地可能不同,这是正常的,不是接口不准。手机号归属地是跟着 SIM 卡走的,跟网络 IP 归属地是两码事,别混。
二、接口就绪检查
免费版能力(无需注册,60 次/分钟/IP):
GET https://ip9.com.cn/get:查当前请求方 IP 的归属地,返回ip、country、prov、city、isp(运营商)、post_code、area_code、lng/lat(城市中心经纬度)、big_area等GET https://ip9.com.cn/get?ip=<ip>:查指定 IP,IPv4 和 IPv6 都支持- 非法 IP 返回
ret=400,限速返回ret=429
两个坑提前说,都是实测踩出来的:
显式带 User-Agent 请求头。这类免费接口普遍会对「默认特征」的请求做风控(部分库的默认 UA/指纹会返回 403),显式声明一个浏览器样式的 UA 最稳。下面代码里 OkHttp 构建请求时加了一行请求头,照抄即可。
60 次/分钟/IP 的额度,对「登录页展示 + 登录安全校验」这种低频场景绰绰有余,但别把每个页面请求都去打一次接口,客户端里按出口 IP 缓存一下,见「四」。
清单:AndroidManifest.xml 加 <uses-permission android:name="android.permission.INTERNET"/>;Gradle 依赖 OkHttp 和协程(okhttp:4.12.0、kotlinx-coroutines-android);解析 JSON 用系统自带的 org.json,不用额外引库。
三、Kotlin 实现
先写一个查询封装,输入空就是查当前网络,也可以传指定 IP:
kotlin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.util.concurrent.TimeUnit
data class NetLoc(
val ip: String,
val countryCode: String, // cn / us / jp ...
val prov: String, // 省份,海外 IP 一般为空
val city: String, // 城市
val isp: String // 运营商
) {
// 展示文案:国内显示「广东 广州 · 中国电信」,海外只有国家,别硬拼空省市
val display: String
get() = when {
prov.isNotEmpty() -> "$prov $city · $isp"
else -> "$countryCode · $isp"
}
}
object IpGeo {
private val client = OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.build()
/** 查当前网络的出口 IP 归属地;也可传 ip 参数查指定 IP */
suspend fun query(ip: String? = null): NetLoc? = withContext(Dispatchers.IO) {
val url = if (ip.isNullOrBlank()) {
"https://ip9.com.cn/get" // 无参 = 查当前请求方
} else {
"https://ip9.com.cn/get?ip=$ip"
}
try {
val request = Request.Builder()
.url(url)
// 显式指定 UA:部分客户端默认请求特征会被风控拦(403)
.header("User-Agent", "Mozilla/5.0 (Linux; Android 13) MyApp/1.0")
.build()
client.newCall(request).execute().use { resp ->
if (!resp.isSuccessful) return@withContext null
val body = resp.body?.string() ?: return@withContext null
val json = JSONObject(body)
if (json.optInt("ret") != 200) return@withContext null
val d = json.getJSONObject("data")
NetLoc(
ip = d.optString("ip"),
countryCode = d.optString("country_code"),
prov = d.optString("prov"),
city = d.optString("city"),
isp = d.optString("isp")
)
}
} catch (e: Exception) {
null // 超时、断网一律返回 null,由调用方降级,绝不崩
}
}
}调用方示例一:工具页 / 客服页展示当前网络位置:
kotlin
// Activity 里,协程环境(lifecycleScope 来自 lifecycle-runtime-ktx)
btnCheck.setOnClickListener {
lifecycleScope.launch {
tvNet.text = "查询中…"
val loc = IpGeo.query() // 查当前网络
tvNet.text = loc?.let {
"当前网络:${it.display}\n出口 IP:${it.ip}"
} ?: "网络位置查询失败,已跳过展示"
}
}调用方示例二:登录安全提示。把上次登录的省份存本地,登录时网络省份对不上就提醒,但不直接拦死:
kotlin
private fun doLogin() {
lifecycleScope.launch {
val loc = IpGeo.query()
val lastProv = prefs.getString("last_login_prov", "") ?: ""
if (loc != null && lastProv.isNotEmpty() && loc.prov.isNotEmpty() && loc.prov != lastProv) {
// 和常用地不一致:弹提示 + 让用户确认,必要时走短信/人脸二次验证
showConfirmDialog("当前网络位于 ${loc.prov}${loc.city},与您常用登录地($lastProv)不一致," +
"如果是本人操作请继续,否则建议先修改密码")
}
prefs.edit().putString("last_login_prov", loc?.prov ?: lastProv).apply()
// ……继续正常登录流程
}
}代码就这么多,核心逻辑三块:OkHttp 带 UA 发请求、org.json 解析、结果兜底。没有第二种语言的实现,高并发网关上的风控判断是后端的事,客户端只负责展示和轻提示。
四、落地注意事项
- 查询失败必须降级。断网、接口抖动时
query()返回 null,页面就跳过展示、登录也不拦——安全提示是加分项,不能因为查归属地把用户卡在登录页外面。 - 缓存。同一台手机出口 IP 短期内不变,登录页每次进来都打一次接口没必要。简单做法:内存里按「出口 IP + 时间戳」存一份,10 分钟内不重复请求;App 里出口 IP 可以从上次结果里取。60 次/分钟/IP 的免费额度,加上缓存后基本用不完。
- 别把经纬度当定位。返回的
lng/lat是城市中心点,不是用户位置。在 UI 上写清楚「网络位置仅供参考」,拿它当精确 GPS 用会出事。 - 「异地」判断别一刀切。出差、公司 WiFi 共享出口、校园网,都会让归属地「漂移」。客户端提示「与常用地不一致」就够了,真要上封号/拦截,必须在服务端结合账号行为(设备、登录频率、常用地历史)综合判断,否则天天误伤。
- 免费版不含
ip_type/ip_asn(那是 VIP 版字段)。想进一步判断「机房 IP 登录」这种高危场景,后端可以上 VIP 版查ip_type,客户端不需要。
总结
Android 上查 IP 归属地,记住一句话:手机拿到的是内网 IP,必须让外部接口告诉你出口 IP。用 IP9 免费接口 https://ip9.com.cn/get 无参调用,一次返回出口 IP + 国家/省市/运营商,免注册免 key,Kotlin 里 OkHttp 加个浏览器 UA 就能跑通,适合登录页网络位置展示、登录安全提示、客服外勤工具的联网环境标记。接口文档和字段说明见官网 https://www.ip9.com.cn ,查指定 IP 用 https://ip9.com.cn/get?ip=xxx 即可。落地时把失败降级和缓存做好,这功能就稳了。