在spring boot 项目中获取请求的ip与详细地址,很多网站app 中都已经新增了ip 地址显示,大家也可以用在自己的开发中,显得更高级。
如果使用本地ip 解析的话,我们将会借助ip2region,该项目维护了一份较为详细的本地ip 地址对应表,如果为了离线环境的使用,需要导入该项目依赖,并指定版本,不同版本的方法可能存在差异。
org.lionsoul
ip2region
2.6.4
GitHub项目地址:https://github.com/lionsoul2014/ip2region
我们首先需要下载一个 ip2region.xdb 的文件
下载地址:https://github.com/lionsoul2014/ip2region/blob/master/data/ip2region.xdb
打开后点击如图的Download图标即可下载。
下载完成后,需要将该文件放到我们的项目中。
ps:我是直接放到服务器的,因为放在项目的资源文件夹下,当我们调试的时候使用Java Spring自带的工具去获取该文件的绝对路径时,没有任何问题,能够正常获取到。但是当我们打成jar包部署到测试或者开发环境,就会拿不到该文件,具体情况可以看 spring boot集成,db放在resource下,打jar包后可以加载资源,但search不生效。本地debug生效 该Issues,如果不想麻烦的话直接放在服务器上吧,这样我们就能直接配好绝对路径了,少走弯路。
在使用时需要将 xdb 文件下载到工程文件目录下,使用ip2region即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:
vIndex 索引缓存: 使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。
xdb 整个文件缓存: 将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。
/**
* ip查询
*/
@Slf4j
public class IPUtil {
private static final String UNKNOWN = "unknown";
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");
if (!StringUtils.hasLength(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (!StringUtils.hasLength(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (!StringUtils.hasLength(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (!StringUtils.hasLength(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED");
}
if (!StringUtils.hasLength(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_CLUSTER_CLIENT_IP");
}
if (!StringUtils.hasLength(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (!StringUtils.hasLength(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_FORWARDED_FOR");
}
if (!StringUtils.hasLength(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_FORWARDED");
}
if (!StringUtils.hasLength(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_VIA");
}
if (!StringUtils.hasLength(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("REMOTE_ADDR");
}
if (!StringUtils.hasLength(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
int index = ip.indexOf(",");
if (index != -1) {
ip = ip.substring(0, index);
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
public static String getAddr(String ip){
String dbPath = "src/main/resources/ip2region/ip2region.xdb";
// 1、从 dbPath 加载整个 xdb 到内存。
byte[] cBuff;
try {
cBuff = Searcher.loadContentFromFile(dbPath);
} catch (Exception e) {
log.info("failed to load content from `%s`: %s\n", dbPath, e);
return null;
}
// 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
Searcher searcher;
try {
searcher = Searcher.newWithBuffer(cBuff);
} catch (Exception e) {
log.info("failed to create content cached searcher: %s\n", e);
return null;
}
// 3、查询
try {
String region = searcher.searchByStr(ip);
return region;
} catch (Exception e) {
log.info("failed to search(%s): %s\n", ip, e);
}
return null;
}
import org.lionsoul.ip2region.xdb.Searcher;
import java.io.IOException;
public class SearcherIPUtils {
public static String getCachePosition(String ip) {
return SearcherIPUtils.getCachePosition("src/main/resources/ip2region/ip2region.xdb", ip, true);
}
public static String getPosition(String dbPath,String ip,boolean format) {
// 1、创建 searcher 对象
Searcher searcher = null;
try {
searcher = Searcher.newWithFileOnly(dbPath);
} catch (IOException e) {
throw new RuntimeException(e);
}
// 2、查询
try {
String region = searcher.search(ip);
if (format){
return region;
}
String[] split = region.split("\\|");
String s = split[0] + split[2] + split[3];
return s;
} catch (Exception e) {
throw new RuntimeException(e);
}
// 3、备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。
}
/**
* @Description : 缓存索引
* @Author : mabo
*/
public static String getIndexCachePosition(String dbPath, String ip, boolean format) {
Searcher searcher = null;
// 1、从 dbPath 中预先加载 VectorIndex 缓存,并且把这个得到的数据作为全局变量,后续反复使用。
byte[] vIndex;
try {
vIndex = Searcher.loadVectorIndexFromFile(dbPath);
} catch (Exception e) {
throw new RuntimeException(e);
}
// 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
try {
searcher = Searcher.newWithVectorIndex(dbPath, vIndex);
} catch (Exception e) {
throw new RuntimeException(e);
}
// 2、查询
try {
String region = searcher.search(ip);
if (format){
return region;
}
String[] split = region.split("\\|");
String s = split[0] + split[2] + split[3];
return s;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @Description : 缓存整个文件
* @Author : mabo
*/
public static String getCachePosition(String dbPath,String ip,boolean format) {
// 1、从 dbPath 加载整个 xdb 到内存。
byte[] cBuff;
try {
cBuff = Searcher.loadContentFromFile(dbPath);
} catch (Exception e) {
throw new RuntimeException(e);
}
// 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
Searcher searcher;
try {
searcher = Searcher.newWithBuffer(cBuff);
} catch (Exception e) {
throw new RuntimeException(e);
}
// 3、查询
try {
String region = searcher.search(ip);
if (format){
return region;
}
String[] split = region.split("\\|");
String s = split[0] + split[2] + split[3];
return s;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
这里我们将ip 解析封装成一个工具类,包含获取IP和ip 地址解析两个方法,ip 的解析可以在请求中获取。获取到ip后,需要根据ip ,在xdb 中查找对应的IP地址的解析,由于是本地数据库可能存在一定的缺失,部分ip 存在无法解析的情况。
whois.pconline.com
的IP解析如果想要获取更加全面的ip 地址信息,可使用在线数据库,这里提供的是 whois.pconline.com
的IP解析,该IP解析在我的使用过程中表现非常流畅,而且只有少数的ip 存在无法解析的情况。
@Slf4j
public class AddressUtils {
// IP地址查询
public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp";
// 未知地址
public static final String UNKNOWN = "XX XX";
public static String getRealAddressByIP(String ip) {
String address = UNKNOWN;
// 内网不查询
if (internalIp(ip)) {
return "内网IP";
}
if (true) {
try {
String rspStr = sendGet(IP_URL, "ip=" + ip + "&json=true" ,"GBK");
if (StrUtil.isEmpty(rspStr)) {
log.error("获取地理位置异常 {}" , ip);
return UNKNOWN;
}
JSONObject obj = JSONObject.parseObject(rspStr);
String region = obj.getString("pro");
String city = obj.getString("city");
return String.format("%s %s" , region, city);
} catch (Exception e) {
log.error("获取地理位置异常 {}" , ip);
}
}
return address;
}
public static String sendGet(String url, String param, String contentType) {
StringBuilder result = new StringBuilder();
BufferedReader in = null;
try {
String urlNameString = url + "?" + param;
log.info("sendGet - {}" , urlNameString);
URL realUrl = new URL(urlNameString);
URLConnection connection = realUrl.openConnection();
connection.setRequestProperty("accept" , "*/*");
connection.setRequestProperty("connection" , "Keep-Alive");
connection.setRequestProperty("user-agent" , "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
connection.connect();
in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType));
String line;
while ((line = in.readLine()) != null) {
result.append(line);
}
log.info("recv - {}" , result);
} catch (ConnectException e) {
log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e);
} catch (SocketTimeoutException e) {
log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e);
} catch (IOException e) {
log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e);
} catch (Exception e) {
log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e);
} finally {
try {
if (in != null) {
in.close();
}
} catch (Exception ex) {
log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
}
}
return result.toString();
}
/**
* 检查是否为内部IP地址
*
* @param ip IP地址
* @return 结果
*/
public static boolean internalIp(String ip) {
byte[] addr = textToNumericFormatV4(ip);
return internalIp(addr) || "127.0.0.1".equals(ip);
}
/**
* 检查是否为内部IP地址
*
* @param addr byte地址
* @return 结果
*/
private static boolean internalIp(byte[] addr) {
if (ArrayUtils.isEmpty(addr) || addr.length < 2) {
return true;
}
final byte b0 = addr[0];
final byte b1 = addr[1];
// 10.x.x.x/8
final byte SECTION_1 = 0x0A;
// 172.16.x.x/12
final byte SECTION_2 = (byte) 0xAC;
final byte SECTION_3 = (byte) 0x10;
final byte SECTION_4 = (byte) 0x1F;
// 192.168.x.x/16
final byte SECTION_5 = (byte) 0xC0;
final byte SECTION_6 = (byte) 0xA8;
switch (b0) {
case SECTION_1:
return true;
case SECTION_2:
return (b1 >= SECTION_3 && b1 <= SECTION_4);
case SECTION_5:
return (b1 == SECTION_6);
default:
return false;
}
}
/**
* 将IPv4地址转换成字节
*
* @param text IPv4地址
* @return byte 字节
*/
private static byte[] textToNumericFormatV4(String text) {
if (text.length() == 0) {
return new byte[0];
}
byte[] bytes = new byte[4];
String[] elements = text.split("\\.", -1);
try {
long l;
int i;
switch (elements.length) {
case 1:
l = Long.parseLong(elements[0]);
if ((l < 0L) || (l > 4294967295L)) {
return new byte[0];
}
bytes[0] = (byte) (int) (l >> 24 & 0xFF);
bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF);
bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
bytes[3] = (byte) (int) (l & 0xFF);
break;
case 2:
l = Integer.parseInt(elements[0]);
if ((l < 0L) || (l > 255L)) {
return new byte[0];
}
bytes[0] = (byte) (int) (l & 0xFF);
l = Integer.parseInt(elements[1]);
if ((l < 0L) || (l > 16777215L)) {
return new byte[0];
}
bytes[1] = (byte) (int) (l >> 16 & 0xFF);
bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
bytes[3] = (byte) (int) (l & 0xFF);
break;
case 3:
for (i = 0; i < 2; ++i) {
l = Integer.parseInt(elements[i]);
if ((l < 0L) || (l > 255L)) {
return new byte[0];
}
bytes[i] = (byte) (int) (l & 0xFF);
}
l = Integer.parseInt(elements[2]);
if ((l < 0L) || (l > 65535L)) {
return new byte[0];
}
bytes[2] = (byte) (int) (l >> 8 & 0xFF);
bytes[3] = (byte) (int) (l & 0xFF);
break;
case 4:
for (i = 0; i < 4; ++i) {
l = Integer.parseInt(elements[i]);
if ((l < 0L) || (l > 255L)) {
return new byte[0];
}
bytes[i] = (byte) (int) (l & 0xFF);
}
break;
default:
return new byte[0];
}
} catch (NumberFormatException e) {
return new byte[0];
}
return bytes;
}
}
只需要通过GET请求该链接 http://ip-api.com/json/ 并后面拼接你的ip,如 http://ip-api.com/json/24.48.0.1 即可。
也能通过添加lang参数让该接口返回中文。如 http://ip-api.com/json/24.48.0.1?lang=zh-CN,就可以返回中文数据了。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
@Slf4j
@Component
public class IpUtil {
private static RestTemplate restTemplate;
private final RestTemplate template;
public static final String IP_API_URL = "http://ip-api.com/json/";
public IpUtil(RestTemplate restTemplate) {
this.template = restTemplate;
}
/**
* 初始化 RestTemplate
*/
@PostConstruct
public void init() {
setRestTemplate(this.template);
}
/**
* 初始化 RestTemplate
*/
private static void setRestTemplate(RestTemplate template) {
restTemplate = template;
}
/**
* 通过ip地址获取所属国家-在线方式
*
* @param ip ip地址
* @return 国家信息
*/
public static CountryInfo getCountryByIpOnline(String ip) {
ResponseEntity entity = restTemplate.getForEntity(
IP_API_URL + ip + "?lang=zh-CN",
CountryInfo.class
);
return entity.getBody();
}
/**
* 在线方式接收类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class CountryInfo {
private String query;
private String status;
private String country;
private String countryCode;
private String region;
private String regionName;
private String city;
private String zip;
private String lat;
private String lon;
private String timezone;
private String isp;
private String org;
private String as;
}
}
那么在开发的什么流程获取ip 地址是比较合适的,这里就要用到我们的拦截器了。拦截进入服务的每个请求,进行前置操作,在进入时就完成请求头的解析,ip 获取以及ip 地址解析,这样在后续流程的全环节,都可以复用ip 地址等信息。
/**
* 对ip 进行限制,防止IP大量请求
*/
@Slf4j
@Configuration
public class IpUrlLimitInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) {
//更新全局变量
Constant.IP = IPUtil.getIpAddr(httpServletRequest);
Constant.IP_ADDR = AddressUtils.getRealAddressByIP(Constant.IP);
Constant.URL = httpServletRequest.getRequestURI();
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) {
//通过本地获取
// 获得ip
// String ip = IPUtil.getIpAddr(httpServletRequest);
//解析具体地址
// String addr = IPUtil.getAddr(ip);
//通过在线库获取
// String ip = IpUtils.getIpAddr(httpServletRequest);
// String ipaddr = AddressUtils.getRealAddressByIP(ipAddr);
// log.info("IP >> {},Address >> {}",ip,ipaddr);
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
}
}
如果想要执行我们的ip 解析拦截器,需要在spring boot的视图层进行拦截才会触发我们的拦截器。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
IpUrlLimitInterceptor ipUrlLimitInterceptor;
//执行ip拦截器
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(ipUrlLimitInterceptor)
// 拦截所有请求
.addPathPatterns("/**");
}
}
通过这样的一套流程下来,我们就能实现对每一个请求进行ip 获取、ip解析,为每个请求带上具体ip地址的小尾巴。
这里我推荐我们项目中都统一采用先离线后在线方式兜底的写法来处理。因为谁也不能保证离线的就一定能查询到具体的归属地,如果我们使用离线方式查询不到具体的归属地(获取结果为null),那么我建议再使用在线查询的方式来做一个兜底处理,如果都拿不到,再返回空,或者根据自己的业务具体处理。