通过 Request 请求获取真实 IP 地址以及对应省份城市
1. 获取真实 IP 地址
1.1 代码
代码如下,这里的 CommonUtil.isBlank()
为封装的判空方法
public static String getIpAddress(HttpServletRequest request) {
// 首先, 获取 X-Forwarded-For 中的 IP 地址,它在 HTTP 扩展协议中能表示真实的客户端 IP
String ipAddress = request.getHeader("X-Forwarded-For");
if (CommonUtil.isNotBlank(ipAddress) && !"unknown".equalsIgnoreCase(ipAddress)) {
// 多次反向代理后会有多个 ip 值,第一个 ip 才是真实 ip, 例: X-Forwarded-For: client, proxy1, proxy2,proxy…
int index = ipAddress.indexOf(",");
if (index != -1) {
return ipAddress.substring(0, index);
}
return ipAddress;
}
// 如果 X-Forwarded-For 获取不到, 就去获取 X-Real-IP
ipAddress = request.getHeader("X-Real-IP");
if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
// 如果 X-Real-IP 获取不到, 就去获取 Proxy-Client-IP
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
// 如果 Proxy-Client-IP 获取不到, 就去获取 WL-Proxy-Client-IP
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
// 如果 WL-Proxy-Client-IP 获取不到, 就去获取 HTTP_CLIENT_IP
ipAddress = request.getHeader("HTTP_CLIENT_IP");
}
if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
// 如果 HTTP_CLIENT_IP 获取不到, 就去获取 HTTP_X_FORWARDED_FOR
ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
// 都获取不到, 最后才通过 request.getRemoteAddr() 获取IP
ipAddress = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ipAddress) ? "127.0.0.1" : ipAddress;;
}
1.2 解释
1、首先,获取 X-Forwarded-For
中第 0 位的 IP 地址,它在 HTTP 扩展协议中能表示真实的客户端 IP,如下例:
X-Forwarded-For: client, proxy1, proxy2, proxy…
2、如果 X-Forwarded-For
获取不到,就去获取 X-Real-IP
,X-Real-IP
获取不到,就依次获取 Proxy-Client-IP
、WL-Proxy-Client-IP
、HTTP_CLIENT_IP
、 HTTP_X_FORWARDED_FOR
。最后获取不到才通过 request.getRemoteAddr()
获取 IP
X-Real-IP
: 记录请求的客户端真实 IP,与X-Forwarded-For
类似Proxy-Client-IP
: 代理客户端的 IP,如果客户端真实 IP 获取不到的时候,就只能获取代理客户端的 IP 了WL-Proxy-Client-IP
: 在 Weblogic 下获取真实 IP 所用的的参数HTTP_CLIENT_IP
、HTTP_X_FORWARDED_FOR
: 可以理解为X-Forwarded-For
, 它们是 PHP 中的用法
3、在服务器上通过 request.getRemoteAddr()
获取服务器的地址时,获取到的是 IPV6 的 0:0:0:0:0:0:0:1,需要转换为 IPV4 的 127.0.0.1
1.3 Nginx 配置请求头参数
server {
listen 8081;
server_name localhost;
location / {
root html/resource-nav;
index index.html index.htm;
}
location ~ /resNav {
#代理请求头相关
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-Ip $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://ip:port;
}
}
2. 通过 IP 地址获取省份城市信息
分为两种方式,在线和离线:
1、使用在线第三方提供的 api:
- ip-api.com
- ip.taotao.com
- 百度地图 api
- 新浪 iplookup
2、使用离线查询方式:
- 纯真库
- GeoLite2
- 埃文科技
具体的数据丰富性、准确性和查询速度可自行搜集相关资料。由于 GeoLite2 免费,且离线查询速度更快和稳定,同时不限制 API 并发数等,这里使用 GeoLite2 来获取省份城市信息,同时数据丰富性也比较高
2.1 下载 GeoLite2 City 库
GeoLite 数据库是 MaxMind 公司旗下的 ,GeoLite 数据库有开源版本和收费版本,这里使用开源版本,GeoLite 目前已经更新到 2 了,所以下载 GeoLite2 City 库。下载地址如下:GeoLite2 Free Geolocation Data | MaxMind Developer Portal
点击页面中的 Download Files
未登录的话会跳转到登录页面
没有账户的话点击创建
这里会有几种账户形式,选择登录免费的 GeoLite2 数据库和 Web 服务
填写完对应的信息后,会发一封设置密码的邮件,点击链接设置密码
完成后点击进行登录
输入用户名密码进行登录,用户名就是邮箱地址
选择下载数据库
选择 GZIP 下载
下载完成后会得到一个 tar 包文件
解压后里面就是我们需要的数据库文件(Windows 可用 7-Zip 解压)
2.2 使用
2.2.1将文件放入项目根路径下
2.2.2 引入依赖
好像 3.0 版本以上最低支持 JDK 11,假如是 JDK 8 的话最高使用 2.16.1 即可
<!-- GeoIP2 -->
<dependency>
<groupId>com.maxmind.geoip2</groupId>
<artifactId>geoip2</artifactId>
<version>2.16.1</version>
</dependency>
2.2.3 代码
这里的 new DatabaseReader.Builder(database).build()
支持两种类型,一种是 File,一种是 InputStream。本地项目两种皆可,但打包到服务器上运行时获取 File 类型文件可能会存在问题,最好通过流的方式来获取构建
public class Test {
public static void main(String[] args) throws IOException, GeoIp2Exception {
// 读取数据库文件
ClassPathResource classPathResource = new ClassPathResource("GeoLite2-City.mmdb");
InputStream database = database = classPathResource.getInputStream();
// 创建数据库
DatabaseReader reader = new DatabaseReader.Builder(database).build();
// 获取 IP 地址信息
InetAddress ipAddress = InetAddress.getByName("139.227.47.35");
// 获取查询信息
CityResponse response = reader.city(ipAddress);
// 国家信息
Country country = response.getCountry();
System.out.println(country.getIsoCode()); // 'CN'
System.out.println(country.getName()); // 'China'
// {de=China, ru=Китай, pt-BR=China, ja=中国, en=China, fr=Chine, zh-CN=中国, es=China}
System.out.println(country.getNames());
System.out.println(country.getNames().get("zh-CN")); // '中国'
// 省级信息
Subdivision subdivision = response.getMostSpecificSubdivision();
System.out.println(subdivision.getIsoCode()); // 'SH'
System.out.println(subdivision.getName()); // 'Shanghai'
// {{en=Shanghai, fr=Municipalité de Shanghai, zh-CN=上海, pt-BR=Xangai}}
System.out.println(subdivision.getNames());
System.out.println(subdivision.getNames().get("zh-CN")); // '上海'
// 城市信息
City city = response.getCity();
System.out.println(city.getName()); // 'Shanghai'
System.out.println(city.getNames().get("zh-CN")); // '上海'
// 邮政编码(国内的可能获取不到)
Postal postal = response.getPostal();
System.out.println(postal.getCode()); // '55423'
// 经纬度
Location location = response.getLocation();
System.out.println(location.getLatitude()); // 纬度 31.2222
System.out.println(location.getLongitude()); // 经度 121.4581
}
}
2.3 封装成工具类
1、实体类
@Data
@TableName("login_geo")
public class LoginGeoDO {
// 主键ID
private String id;
// 国家 ISO 代码
private String countryIsoCode;
// 国家名称
private String countryName;
// 国家中文名称
private String countryZhCnName;
// 省级 ISO 代码, 外国则是同级别地区代码
private String subdivisionIsoCode;
// 省级名称
private String subdivisionName;
// 省级中文名称
private String subdivisionZhCnName;
// 城市名称
private String cityName;
// 城市中文名称
private String cityZhCnName;
// 邮政编码
private String postal;
// 纬度
private double latitude;
// 经度
private double longitude;
// 创建时间
private Timestamp createTime;
// 更新时间
private Timestamp updateTime;
}
2、封装工具类
这里把前面获取 IP 地址的方法也封装进来了,LogUtil 为封装的日志工具类
public class AuthUtil {
private static InputStream database;
private static DatabaseReader reader;
static {
// 读取数据库文件
LogUtil.info("读取数据库文件");
ClassPathResource classPathResource = new ClassPathResource("GeoLite2-City.mmdb");
// 创建数据库
try {
database = classPathResource.getInputStream();
reader = new DatabaseReader.Builder(database).build();
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
}
/**
* 获取 IP 地址
*
* @param request 请求
* @return {@link String}
* @author Fan
* @since 2022/11/28 9:08
*/
public static String getIpAddress(HttpServletRequest request) {
// 首先, 获取 X-Forwarded-For 中的 IP 地址,它在 HTTP 扩展协议中能表示真实的客户端 IP
String ipAddress = request.getHeader("X-Forwarded-For");
if (CommonUtil.isNotBlank(ipAddress) && !"unknown".equalsIgnoreCase(ipAddress)) {
// 多次反向代理后会有多个 ip 值,第一个 ip 才是真实 ip, 例: X-Forwarded-For: client, proxy1, proxy2,proxy…
int index = ipAddress.indexOf(",");
if (index != -1) {
return ipAddress.substring(0, index);
}
return ipAddress;
}
// 如果 X-Forwarded-For 获取不到, 就去获取 X-Real-IP
ipAddress = request.getHeader("X-Real-IP");
if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
// 如果 X-Real-IP 获取不到, 就去获取 Proxy-Client-IP
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
// 如果 Proxy-Client-IP 获取不到, 就去获取 WL-Proxy-Client-IP
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
// 如果 WL-Proxy-Client-IP 获取不到, 就去获取 HTTP_CLIENT_IP
ipAddress = request.getHeader("HTTP_CLIENT_IP");
}
if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
// 如果 HTTP_CLIENT_IP 获取不到, 就去获取 HTTP_X_FORWARDED_FOR
ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
// 都获取不到, 最后才通过 request.getRemoteAddr() 获取IP
ipAddress = request.getRemoteAddr();
}
return ipAddress;
}
/**
* 通过 IP 地址获取地理信息
*
* @param ipAddress IP地址
* @return {@link LoginGeoDO}
* @author Fan
* @since 2022/12/14 16:35
*/
public static LoginGeoDO getGeoInformation(String ipAddress) {
try {
// 获取 IP 地址信息
InetAddress inetAddress = InetAddress.getByName(ipAddress);
// 获取查询信息
CityResponse response = reader.city(inetAddress);
LoginGeoDO loginGeoDO = new LoginGeoDO();
// 国家信息
Country country = response.getCountry();
loginGeoDO.setCountryIsoCode(country.getIsoCode());
loginGeoDO.setCountryName(country.getName());
loginGeoDO.setCountryZhCnName(country.getNames().get("zh-CN"));
// 省级信息
Subdivision subdivision = response.getMostSpecificSubdivision();
loginGeoDO.setSubdivisionIsoCode(subdivision.getIsoCode());
loginGeoDO.setSubdivisionName(subdivision.getName());
loginGeoDO.setSubdivisionZhCnName(subdivision.getNames().get("zh-CN"));
// 城市信息
City city = response.getCity();
loginGeoDO.setCityName(city.getName());
loginGeoDO.setCityZhCnName(city.getNames().get("zh-CN"));
// 邮政编码(国内的可能获取不到)
Postal postal = response.getPostal();
loginGeoDO.setPostal(postal.getCode());
// 经纬度
Location location = response.getLocation();
loginGeoDO.setLatitude(location.getLatitude());
loginGeoDO.setLongitude(location.getLongitude());
return loginGeoDO;
} catch (IOException | GeoIp2Exception exception) {
LogUtil.error(exception.getMessage());
return null;
}
}
}
3. 获取系统、浏览器信息
该类信息一般通过 UA(User Agent)标识来获取。 User Agent 中文名为用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等
先获取请求头中的 User-Agent
String ua = request.getHeader("User-Agent");
引入 UserAgentUtils 依赖
<!-- UserAgentUtils -->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>1.21</version>
</dependency>
使用提供的 UserAgent 类来解析 ua 字符串
UserAgent userAgent = UserAgent.parseUserAgentString(ua);
// 操作系统
userAgent.getOperatingSystem().getName()
// 浏览器
userAgent.getBrowser().getName()