Java集成ip2region2.x实现ip属地功能

效果图/封面

一、前言

如今许多软件如B站、微博、抖音等都加上 IP 归属地防止恶意评论,境外用户显示的是国家,国内的用户显示的省份。兴致一起,我便想自己的博客项目加上此功能。

目前存在几种方式:

  • 方式一:IP地址库ip2region(本文采取的方式)

  • 方式二:使用第三方API(查询效率低,不稳定,容易失效)

    1. 淘宝–IP库地址:https://ip.taobao.com/已经停止服务()
    2. 太平洋IP地址库http://whois.pconline.com.cn/

二、ip2region简介

   ip2region - 准确率99.9%的离线IP地址定位库,0.0x毫秒级查询,ip2region.db数据库只有数MB,提供了java、php、c、python、nodejs、golang、c#等查询绑定和Binary,B树,内存三种查询算法。

  • 地址:

    GitHub地址:https://github.com/lionsoul2014/ip2region

    Gitee地址:https://gitee.com/lionsoul/ip2region

  • 特性/优势

    1. 标准化的数据格式——每个 ip 数据段的 region 信息都固定了格式:国家|区域|省份|城市|ISP
    2. 数据去重和压缩
    3. 极速查询响应——即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别
    4. IP 数据管理框架

三、ip2region的使用

1、下载ip2region.db

将项目从GitHub/Gitee仓库down下来,找到data目录,有三种不同文件格式的ip地址库,进入找到ip2region.xdb,将ip2region.xdb复制出来放到resources下。

2、pom.xml中引入依赖

1
2
3
4
5
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.6.5</version>
</dependency>

3、IpUtils工具类

其中有三个方法,

其一是去获取请求中的真实客户端ip。

其二是通过ip获取城市信息。

其三在服务启动时加载 ip2region.db 这个数据资源到内存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package com.lll.blog.utils;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.lionsoul.ip2region.xdb.Searcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.util.FileCopyUtils;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component
public class IPUtils {

private static Logger logger = LoggerFactory.getLogger(IPUtils.class);

private static Searcher searcher;

/**
* 获取客户端IP地址
*
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = null;
try {
ip = request.getHeader("x-forwarded-for");
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
//根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
log.error("getIpAddress exception:", e);
}
ip = inet.getHostAddress();
}
}
} catch (Exception e) {
logger.error("IPUtils ERROR ", e);
}
return ip;
}

/**
* 根据ip从 ip2region.db 中获取地理位置
*
* @param ip
* @return 地理位置
*/
public static Map getCityInfo(String ip) {
//数据格式: 国家|区域|省份|城市|ISP
//192.168.31.160 0|0|0|内网IP|内网IP
//47.52.236.180 中国|0|香港|0|阿里云
//220.248.12.158 中国|0|上海|上海市|联通
//164.114.53.60 美国|0|华盛顿|0|0
HashMap<String, Object> cityInfo = new HashMap<>();
try {
String searchIpInfo = searcher.search(ip);
String[] splitIpInfo = searchIpInfo.split("\\|");
cityInfo.put("ip",ip);
cityInfo.put("searchInfo", searchIpInfo);
cityInfo.put("country",splitIpInfo[0]);
cityInfo.put("region",splitIpInfo[1]);
cityInfo.put("province",splitIpInfo[2]);
cityInfo.put("city",splitIpInfo[3]);
cityInfo.put("ISP",splitIpInfo[3]);
return cityInfo;
} catch (Exception e) {
log.info("failed to search(%s): %s\n", ip, e);
}
return null;
}

/**
* 在服务启动时加载 ip2region.db 到内存中
* 解决打包jar后找不到 ip2region.db 的问题
*
* @throws Exception 出现异常应该直接抛出终止程序启动,避免后续invoke时出现更多错误
*/
@PostConstruct
private static void initIp2regionResource() {
try {
InputStream inputStream = new ClassPathResource("/ipdb/ip2region.xdb").getInputStream();
byte[] dbBinStr = FileCopyUtils.copyToByteArray(inputStream);
// 创建一个完全基于内存的查询对象
searcher = Searcher.newWithBuffer(dbBinStr);
} catch (Exception e) {
log.info("failed to create content cached searcher: %s\n", e);
}
}

}

4、测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.lll.blog.controller;

import com.lll.blog.utils.IPUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;


/**
* @author Rain
* @date 2022/10/28 15:57
*/
@Controller
public class IpController {

@GetMapping(value = "/testIp")
@ResponseBody
public Object testIp(HttpServletRequest request) {

String ipAddr = IPUtils.getIpAddr(request);
Map cityInfo = IPUtils.getCityInfo(ipAddr);
System.out.println(cityInfo.get("province"));
//数据格式: 国家|区域|省份|城市|ISP
//192.168.31.160 0|0|0|内网IP|内网IP
//47.52.236.180 中国|0|香港|0|阿里云
//220.248.12.158 中国|0|上海|上海市|联通
//164.114.53.60 美国|0|华盛顿|0|0
String ip = "183.213.81.138";
Map result = IPUtils.getCityInfo(ip);
System.out.println(result.get("province"));
return cityInfo;
}
}

四、需求实现-博客项目归属地

项目技术:Springboot + Vue

1、后端

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.lll.blog.controller;

import com.lll.blog.service.SysUserService;
import com.lll.blog.utils.IPUtils;
import com.lll.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

@RestController
@RequestMapping("users")
public class UsersController {

@Autowired
private SysUserService SysUserService;

@GetMapping("getIpRegion")
public Result getIpRegion(HttpServletRequest request){
String ipAddr = IPUtils.getIpAddr(request);
return SysUserService.getIpRegion(ipAddr);
}
}

Service

1
2
3
4
public interface SysUserService {

Result getIpRegion(String ip);
}

Impl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.lll.blog.service.impl;

import com.lll.blog.utils.IPUtils;
import com.lll.blog.vo.ErrorCode;
import com.lll.blog.vo.Result;

import java.util.Map;


@Service
public class SysUserServiceImpl implements SysUserService {

@Override
public Result getIpRegion(String ip) {
Map cityInfo = IPUtils.getCityInfo(ip);
if ("中国".equals(cityInfo.get("country"))){
System.out.println(cityInfo.get("province"));
Object province = cityInfo.get("province");
return Result.success(province);
}
else if ("0".equals(cityInfo.get("country"))){
System.out.println(cityInfo);
if ("内网IP".equals(cityInfo.get("ISP"))){
return Result.success("内网");
}
return Result.success("未知");
}
else {
System.out.println(cityInfo.get("country"));
return Result.success(cityInfo.get("country"));
}
}

}

2、前端

(下面仅仅展示部分关键代码)

api

1
2
3
4
5
export function getIpRegionInfo() {
return request({
url: '/users/getIpRegion',
method: 'get'
})

vue页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<template>
<el-card>
<div class="me-author-description" v-if="!user.login">
<span><i class="el-icon-location-outline"></i> &nbsp;IP属地:{{ip}}</span>
</div>
</el-card>
</template>


<script>
import {getIpRegionInfo} from '@/api/login'

export default {
name: 'CardMe',
created() {
this.getIpRegionInfo()
},
data() {
return {
ip: null,
}
},
methods: {
getIpRegionInfo() {
getIpRegionInfo().then((data => {
this.ip = data.data
})).catch(error => {
if (error !== 'error') {
that.$message({type: 'error', message: 'ip归属地获取失败!', showClose: true})
}
})
}
}
}
</script>

五、扩展

1、好文

2、待尝试