一、背景

最近项目来了个新需求,要记录用户每次登录项目时所在的省份/城市信息。

网上有了解过,对于这个功能是如何实现的,一般有两种方法:GPS 定位的信息用户 IP 地址。对于这次需求我采用了用户 IP 地址来完成功能的实现,因为不是每个客户端都会打开 GPS,而且有时并不太需要太精确的位置(到城市这个级别即可)。

二、Ip2region

2.1 介绍

ip2region 是一个离线IP地址定位库和IP定位数据管理框架,10微秒级别的查询效率,提供了众多主流编程语言的 xdb 数据生成和查询客户端实现。

⭐️Gitee地址:ip2region

🎁GitHub地址:ip2region

2.2 快速上手

第一步,将整个项目down下来,找到data目录,进入

这里有三份ip地址库,我们将ip2region.xdb复制出来,等下我们的java项目中需要使用到。

图片

第二步,创建maven项目,引入依赖

pom.xml依赖如下(我创建的是springboot脚手架项目,都一样):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- ip2region离线IP地址定位库和IP定位数据管理 -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.6.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

加好依赖后,在resources目录下创建ip2region文件夹,把上面的ip2region.xdb文件放进去。

第三步,编写测试类

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
package com.apps;

import org.junit.Test;
import org.lionsoul.ip2region.xdb.Searcher;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

@SpringBootTest
public class Ip2regionApplicationTests {

@Test
public void contextLoads() {

// 创建searcher对象,完全基于文件的查询
String dbPath = Objects.requireNonNull(Ip2regionApplicationTests.class.getResource("/ip2region.xdb")).getPath();
// 空引用
Searcher searcher = null;

try {
// 创建
searcher = Searcher.newWithFileOnly(dbPath);
} catch (IOException e) {
System.out.printf("无法创建搜索器`%s`: %s\n", dbPath, e);
return;
}

// 查询
String ip = "183.14.90.103";

try {
// 开始时间
long sTime = System.nanoTime();
// 区域
String region = searcher.search(ip);
// 用时
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
// 打印
System.out.printf("{区域: %s, ioCount: %d, took: %d}",region, searcher.getIOCount(),cost);
} catch (Exception e) {
System.out.printf("无法搜索(%s): %s\n", ip, e);
}

}

}

输出结果如下:

图片

这样就完成了根据用户的IP地址来获取所在的省份/城市信息。

三、项目实现

3.1 思路分析

通过上面简单的例子我们已经可以通过ip获取地域了,那么接下来将实现如何监控Controller接口的访问地址。

首先,在一个项目中肯定有很多接口,所以我们不能直接在接口中写代码的方式去实现,这样代码复杂度、耦合度太高。所以我打算在这里使用注解切面的方式实现,只需要在接口方法上加上 @Ip 注解就可以实现。

其次,有些项目中会使用 Nginx 等反向代理软件,则不能通过 request.getRemoteAddr() 获取 IP地址 ,如果使用了多级反向代理的话, X-Forwarded-For 的值并不止一个,而是一串 IP地址X-Forwarded-For 中第一个非 unknown 的有效 IP字符串 ,则为 真实IP地址

3.2 配置文件

SpringBoot项目pom.xml文件:

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
<dependencies>
<!-- web启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- ip2region离线IP地址定位库和IP定位数据管理 -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.6.4</version>
</dependency>
<!-- aop切面 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 用于读取ip2region.xdb文件使用 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<!-- test启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

3.3 项目代码

项目结构

图片

SpringbootIpApplication.java

1
2
3
4
5
6
7
8
9
10
11
package com.example.springbootip;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringbootIpApplication {

public static void main(String[] args) {
SpringApplication.run(SpringbootIpApplication.class, args);
}

}

TestController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.springbootip.controller;
import com.example.springbootip.ip2region.Ip;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/hello")
@Ip
public String hello() {
return "hello";
}
}

Ip.java

1
2
3
4
5
6
7
8
9
package com.example.springbootip.ip2region;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Ip {
}

IpAspect.java

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
package com.example.springbootip.ip2region;
import com.example.springbootip.util.AddressUtil;
import com.example.springbootip.util.HttpContextUtil;
import com.example.springbootip.util.IPUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.text.MessageFormat;
@Aspect
@Component
public class IpAspect {

@Pointcut("@annotation(com.example.springbootip.ip2region.Ip)")
public void pointcut() {
// do nothing
}

@Around("pointcut()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
// 获取请求对象
HttpServletRequest request = HttpContextUtil.getHttpServletRequest();
// 获取地址
String ip = IPUtil.getIpAddr(request);
// 打印信息
System.out.println(MessageFormat.format("当前IP为:[{0}];当前IP地址解析出来的地址为:[{1}]", ip, AddressUtil.getCityInfo(ip)));
return point.proceed();
}
}

AddressUtil.java

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
package com.example.springbootip.util;
import org.apache.commons.io.FileUtils;
import org.lionsoul.ip2region.xdb.Searcher;
import java.io.File;
import java.text.MessageFormat;
import java.util.Objects;
public class AddressUtil {

/**
* 当前记录地址的本地DB
*/
private static final String TEMP_FILE_DIR = "/home/admin/app/";

/**
* 根据IP地址查询登录来源
* @param ip
* @return 登录来源
*/
public static String getCityInfo(String ip) {
try {
// 获取当前记录地址位置的文件
String dbPath = Objects.requireNonNull(AddressUtil.class.getResource("/ip2region/ip2region.xdb")).getPath();
File file = new File(dbPath);

//如果当前文件不存在,则从缓存中复制一份
if (!file.exists()) {
dbPath = TEMP_FILE_DIR + "ip.db";
System.out.println(MessageFormat.format("当前目录为:[{0}]", dbPath));
file = new File(dbPath);
FileUtils.copyInputStreamToFile(Objects.requireNonNull(AddressUtil.class.getClassLoader().getResourceAsStream("classpath:ip2region/ip2region.xdb")), file);
}

//创建查询对象
Searcher searcher = Searcher.newWithFileOnly(dbPath);

//开始查询
return searcher.search(ip);
} catch (Exception e) {
e.printStackTrace();
}
//默认返回空字符串
return "";
}

// 测试用的
public static void main(String[] args) {
System.out.println(getCityInfo("1.2.3.4"));
}

}

HttpContextUtil.java

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
package com.example.springbootip.util;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;
/**
* @desc 全局获取HttpServletRequest、HttpServletResponse
*/
public class HttpContextUtil {
// util不要公开构造
private HttpContextUtil() {
}

// 获取请求对象
public static HttpServletRequest getHttpServletRequest() {
// 判断是否存在
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}

// 获取响应对象
public static HttpServletResponse getHttpServletResponse() {
// 判断是否存在
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
}
}

IPUtil.java

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
package com.example.springbootip.util;
import javax.servlet.http.HttpServletRequest;
/**
* @desc 查询当前访问的IP地址
*/
public class IPUtil {

private static final String UNKNOWN = "unknown";

// util不要公开构造
protected IPUtil() {
}

/**
* 获取 IP地址
* 使用 Nginx等反向代理软件, 则不能通过 request.getRemoteAddr()获取 IP地址
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,
* X-Forwarded-For中第一个非 unknown的有效IP字符串,则为真实IP地址
*/
public static String getIpAddr(HttpServletRequest request) {

String ip = request.getHeader("x-forwarded-for");

// equalsIgnoreCase方法用于字符串比较,不考虑大小写
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}

return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}

}

打印结果

由于访问路径是:http://127.0.0.1:8080/test/hello,所以本地解析出来的是内网

图片

好,记录完成!