title: 通过 Request 请求获取真实 IP 地址以及对应省份城市和系统浏览器信息
date: 2022-12-16 16:20:26
tags:
- GeoIP2
- UserAgentUtils
categories: - 开发实践
cover: https://cover.png
feature: false
1. 获取真实 IP 地址
1.1 代码
代码如下,这里的 CommonUtil.isBlank()
为封装的判空方法
public static String getIpAddress(HttpServletRequest request) {// 首先, 获取 X-Forwarded-For 中的 IP 地址,它在 HTTP 扩展协议中能表示真实的客户端 IPString 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-IPipAddress = request.getHeader("X-Real-IP");if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {// 如果 X-Real-IP 获取不到, 就去获取 Proxy-Client-IPipAddress = request.getHeader("Proxy-Client-IP");}if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {// 如果 Proxy-Client-IP 获取不到, 就去获取 WL-Proxy-Client-IPipAddress = request.getHeader("WL-Proxy-Client-IP");}if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {// 如果 WL-Proxy-Client-IP 获取不到, 就去获取 HTTP_CLIENT_IPipAddress = request.getHeader("HTTP_CLIENT_IP");}if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {// 如果 HTTP_CLIENT_IP 获取不到, 就去获取 HTTP_X_FORWARDED_FORipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");}if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {// 都获取不到, 最后才通过 request.getRemoteAddr() 获取IPipAddress = 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.2222System.out.println(location.getLongitude()); // 经度 121.4581}
}
2.3 封装成工具类
1、实体类
@Data
@TableName("login_geo")
public class LoginGeoDO {// 主键IDprivate 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 扩展协议中能表示真实的客户端 IPString 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-IPipAddress = request.getHeader("X-Real-IP");if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {// 如果 X-Real-IP 获取不到, 就去获取 Proxy-Client-IPipAddress = request.getHeader("Proxy-Client-IP");}if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {// 如果 Proxy-Client-IP 获取不到, 就去获取 WL-Proxy-Client-IPipAddress = request.getHeader("WL-Proxy-Client-IP");}if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {// 如果 WL-Proxy-Client-IP 获取不到, 就去获取 HTTP_CLIENT_IPipAddress = request.getHeader("HTTP_CLIENT_IP");}if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {// 如果 HTTP_CLIENT_IP 获取不到, 就去获取 HTTP_X_FORWARDED_FORipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");}if (CommonUtil.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {// 都获取不到, 最后才通过 request.getRemoteAddr() 获取IPipAddress = 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()