ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java后端获取用户真实IP及省市归属地解析实战方案

Java后端获取用户真实IP及省市归属地解析实战方案 做后端开发的估计十有八九都接过这种需求“帮我查一下这个用户的IP是哪里的”、“统计一下各省的访问量”、“这个用户登录异常看看IP归属地”。听起来就是个小事但真动起手来坑不少。光是“怎么拿到用户真实IP”这一步就能拦住不少人更别提后面还要把IP转成省市信息。这篇文章就把我实际做过的方案完整拆开讲一遍。从怎么在Java里正确获取用户IP到两种主流省市解析方案离线IP库和在线API的选型对比再到完整可落地的代码实现和线上踩坑记录一次性讲清楚。无论你是刚入行的初级开发还是正在准备Java面试想补这块实战经验这篇都能直接拿来参考。1. 方案设计先理清楚要解决哪几件事1.1 核心需求拆解“根据用户请求获取IP并解析省市信息”这句话拆开其实包含三个独立的技术点第一从HTTP请求中拿到IP地址。这里面最大的坑是“你拿到的IP不一定是用户真实IP”。如果服务直接暴露在公网没有经过任何反向代理那request.getRemoteAddr()拿到的就是用户IP。但在实际生产环境里请求基本都会经过Nginx、SLB、CDN等多层转发这时候getRemoteAddr()拿到的是上一级代理服务器的IP而不是用户的真实IP。所以需要从X-Forwarded-For、X-Real-IP这些HTTP头里取。第二把IP转成具体的省市信息。这一步有两条技术路线离线方案下载一份IP段和地域的映射数据比如ip2region、纯真IP库在本地用二分查找或内置算法匹配速度快、不依赖外部服务、无费用。在线方案调用第三方HTTP接口比如淘宝IP库、太平洋IP库、百度地图IP定位把IP发过去对方返回JSON格式的省市信息。第三把解析结果落库或返回给前端。这块涉及数据结构设计比如用户表要不要加province、city字段还是单独建一张访问日志表。1.2 方案选型离线为主在线兜底我最终定下来的方案是以离线IP库ip2region为主在线API做兜底。为什么要这么设计并不是拍脑袋。先看离线方案的优势。ip2region目前的数据覆盖量是亿级IP段查询性能极快单次查询在微秒级别而在线API单次请求至少需要几十毫秒网络IO。在QPS高的场景下离线库的优势是碾压级的而且内网部署不依赖公网服务也就不会出现“第三方服务挂了你的接口就跟着挂”的连锁故障。另外离线库不涉及费用在线API通常都有每日调用次数限制即便免费版也有配额上限。但离线方案有个固有问题数据是静态的更新有滞后性。IP段的分配是会变的运营商经常做地址段调整所以离线库的准确率会随着时间推移下降。而在线API由服务商实时维护准确率更高。所以我把两者结合起来先查离线库命中且结果合理就直接用离线库查不到或者结果明显异常比如解析出来是“内网IP”或“未知”再调在线API兜底。这样兼顾了性能和准确率。提示选择方案前先搞清楚自己的业务场景。如果是像本文这种“给用户ID绑定一个省市属性”的中低并发场景单用在线API其实也能跑只是要做好超时和熔断。但如果要做实时统计、每日千万级请求离线库是必须的。先把并发量评估清楚再决定架构复杂度。2. 获取用户真实IP这部分坑最多2.1 为什么不能直接用getRemoteAddr()很多新手上来就写String ip request.getRemoteAddr()然后发现线上拿到的IP全是同一个排查半天才意识到是代理服务器的问题。下面用一张场景化的例子说明。假设用户浏览器访问一个部署在云服务器上的Java应用中间经过了一层Nginx做反向代理。这时候TCP连接是浏览器先连到NginxNginx再连到Tomcat。Tomcat看到的远端IP是Nginx服务器的内网IP比如172.17.0.2。Nginx在转发请求时会把原始请求方的IP写在X-Forwarded-For或X-Real-IP头里。所以处理代理场景的标准做法是优先从请求头里取取不到再退回getRemoteAddr()。2.2 各请求头的优先级与含义常见的几个头需要搞清楚X-Forwarded-For每经过一层代理代理都会把上一级的IP追加到这个头的末尾用英文逗号分隔。比如X-Forwarded-For: 1.2.3.4, 10.0.0.1其中第一个就是原始客户端IP后面的都是链路中的代理IP。X-Real-IPNginx代理时常用这个头传递真实的客户端IP。它通常只包含一个IP。Proxy-Client-IP和WL-Proxy-Client-IPWebLogic等老牌应用服务器会用部分场景下需要考虑兼容。标准做法是从X-Forwarded-For里取第一个非空、非unknown的IP。不过这里要注意一个安全问题客户端是可以伪造X-Forwarded-For头的。如果自己的服务器不信任外层代理而直接信任这个头攻击者可以随便伪造一个IP绕过基于IP的风控策略。所以正确姿势是Nginx层用proxy_set_header X-Real-IP $remote_addr强制覆盖应用层优先读X-Real-IP只有在确认了链路可信的情况下才去解析X-Forwarded-For。2.3 拿到IP后的必要校验无论从哪里取到IP都不能直接用得先做格式校验。实战中经常遇到这些脏数据值为unknown或空字符串。多个IP拼接在一起比如1.2.3.4, 10.0.0.1。非法格式比如写了个abc或者999.1.1.1。IPv6地址。很多线上环境会同时支持IPv4和IPv6如果只做IPv4解析需要单独处理。我的工具类里加了一个简单的IP格式校验用正则表达式过滤。注意这里不要用过于严格的正则否则容易误伤合法的IP比如带前导零的1.2.3.04在某些场景下是合法的。3. 省市解析两种方案的完整实现3.1 离线方案集成ip2regionip2region是目前最主流的Java离线IP库开源免费Gitee上的star量很高。它的原理是把IP段和地域信息的映射关系存到一个二进制数据文件里查询时用内置的二分查找算法效率很高。最新的v2.0版本支持了多语言绑定Java可以直接用官方提供的Searcher类。引入依赖的方式Maven项目在pom.xml里加dependency groupIdorg.lionsoul/groupId artifactIdip2region/artifactId version2.7.0/version /dependency然后把ip2region.xdb数据文件放到src/main/resources目录下。这个文件可以从官方GitHub仓库下载约11MB左右。查询代码的核心逻辑如下import org.lionsoul.ip2region.xdb.Searcher; import org.lionsoul.ip2region.xdb.Loader; import java.io.InputStream; public class IpRegionUtil { private static byte[] XDB_DATA; private static Searcher SEARCHER; static { try (InputStream is IpRegionUtil.class.getClassLoader() .getResourceAsStream(ip2region.xdb)) { if (is null) { throw new RuntimeException(ip2region.xdb not found in classpath); } XDB_DATA is.readAllBytes(); SEARCHER Searcher.newWithBuffer(XDB_DATA); } catch (Exception e) { throw new RuntimeException(ip2region.xdb init failed, e); } } /** * 根据IP地址IPv4点分十进制格式解析省市信息 * return 格式如 中国|0|广东省|深圳市|电信 的字符串 */ public static String region(String ip) { try { return SEARCHER.search(ip); } catch (Exception e) { return null; } } }定位结果的格式是竖线分隔的字符串国家|区域|省份|城市|运营商。比如中国|0|广东省|深圳市|电信。区域在xdb里通常是0可以不关注。如果IP是内网地址或者保留地址段返回结果是0|0|0|内网IP|内网IP这个要单独处理。一个关键性能点是Searcher对象要复用不要每次查询都new一个新的。官方提供了一个Searcher.newWithBuffer(byte[])方法直接用整个数据文件的内存副本初始化后续查询都在内存里做二分查找速度极快。实测单线程每秒可以跑十几万次查询。3.2 在线方案调用淘宝IP库API淘宝IP库是老牌免费IP定位接口了不需要申请key直接发HTTP请求就能用。接口地址是https://ip.taobao.com/outGetIpInfo支持GET和POST参数就是ip和accessKey可选。我自己封装了一个基于JDK 11HttpClient的工具类。用原生JDK的HttpClient是为了减少依赖如果你的项目里已经引入了OkHttp或RestTemplate也可以用那些逻辑是一样的。import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; public class IpApiUtil { private static final String API_URL https://ip.taobao.com/outGetIpInfo?ip; private static final HttpClient HTTP_CLIENT HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .build(); private static final ObjectMapper MAPPER new ObjectMapper(); /** * 调用淘宝IP库返回省份和城市 */ public static String[] getRegionByOnline(String ip) { try { HttpRequest request HttpRequest.newBuilder() .uri(URI.create(API_URL ip)) .timeout(Duration.ofSeconds(3)) .GET() .build(); HttpResponseString response HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() ! 200) { return null; } JsonNode root MAPPER.readTree(response.body()); if (root.path(code).asInt() ! 0) { return null; } JsonNode data root.path(data); String province data.path(region).asText(); String city data.path(city).asText(); return new String[]{province, city}; } catch (Exception e) { return null; } } }接口返回的JSON结构是{code:0,data:{region:广东省,city:深圳市}}。code为0表示成功非0表示失败比如IP格式不对或不在数据库中。注意region字段偶尔会为空结合country字段可以兜底显示国家层级。3.3 为什么要预留一个兜底策略离线库和在线API各有一个无法避免的短板所以才要做兜底。离线库的短板是数据更新滞后。我用2019年的旧版xdb做过一次测试有一个2021年新分配的AWS IP段离线库识别成了“美国”实际上这是用户从中国香港的AWS区域访问的。而在线API用同一IP查询正确返回了“中国香港”。所以离线库的解析结果如果命中不到或者解析出的省份是“未知”就需要走在线API。在线API的短板是慢和有限流。淘宝IP库虽然免费但单IP的QPS限制很严格据说单个IP每秒只能调用一次左右。在高并发场景下如果每个请求都同步调在线接口服务会直接被拖垮。我的做法是加了一个简单的本地缓存Caffeine或ConcurrentHashMap同一个IP在24小时内只查一次外部接口之后的请求直接读缓存。提示如果公司有预算可以考虑使用百度地图IP定位API或高德IP定位API这些商业接口的准确率和稳定性更高但需要申请API Key且按调用量计费。对个人项目和学习Demo用淘宝IP库就够了。4. 完整落地从HTTP请求到省市信息的全流程实现4.1 整体架构与代码结构我把整个功能抽象成了三层结构这样无论是写在一个Servlet、Spring MVC的Controller还是写在一个Filter里都能无缝接入。第一层IP获取层。负责从HttpServletRequest里安全地提取真实IP做格式校验和数据清洗。第二层解析服务层。定义统一的IpRegionService接口内部先查ip2region离线库命中不了走淘宝API在线兜底。对外暴露getRegion(String ip)方法返回一个封装好的RegionInfo对象。第三层存储层。把IP、省份、城市、解析时间写入数据库。这里做了一个轻量级的表设计避免过度设计。RegionInfo对象用Java 17的record定义简洁又不可变public record RegionInfo(String ip, String country, String province, String city, String isp) { public static RegionInfo unknown(String ip) { return new RegionInfo(ip, 未知, 未知, 未知, 未知); } public boolean isUnknown() { return 未知.equals(province) 未知.equals(city); } }4.2 获取真实IP的最终版工具类上面说了那么多坑这里给出一个我在生产环境用过的完整工具类。它处理了多级代理、常见请求头、IPv6地址、非法格式等场景import jakarta.servlet.http.HttpServletRequest; import java.net.InetAddress; import java.net.UnknownHostException; public final class IpUtil { private static final String UNKNOWN unknown; private static final String LOCALHOST_IPV4 127.0.0.1; private static final String LOCALHOST_IPV6 0:0:0:0:0:0:0:1; private static final String IPV4_PATTERN ^(25[0-5]|2[0-4]\\d|[01]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[01]?\\d?\\d)){3}$; private IpUtil() { } /** * 从HttpServletRequest中获取真实客户端IP */ public static String getIpAddr(HttpServletRequest request) { if (request null) { return null; } // 1. 优先取X-Real-IP这是Nginx配置的、由可信代理写入 String ip request.getHeader(X-Real-IP); if (isValidIp(ip)) { return ip; } // 2. 再取X-Forwarded-For取第一个非unknown的IP ip request.getHeader(X-Forwarded-For); if (ip ! null !ip.isEmpty()) { String[] ips ip.split(,); for (String s : ips) { String candidate s.trim(); if (isValidIp(candidate)) { return candidate; } } } // 3. 退回常见的其他代理头 String[] headers {Proxy-Client-IP, WL-Proxy-Client-IP, HTTP_CLIENT_IP, HTTP_X_FORWARDED_FOR}; for (String header : headers) { ip request.getHeader(header); if (isValidIp(ip)) { return ip; } } // 4. 最后退回getRemoteAddr ip request.getRemoteAddr(); if (LOCALHOST_IPV6.equals(ip)) { ip LOCALHOST_IPV4; } return ip; } private static boolean isValidIp(String ip) { if (ip null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) { return false; } if (ip.contains(:)) { // IPv6地址简单校验这里先不处理 return ip.split(:, -1).length 3; } return ip.matches(IPV4_PATTERN); } }这里面有几个细节值得细说。第一X-Forwarded-For里的IP可能不止一个取第一个非unknown的值。为什么不能直接取第一个因为有些代理链路里最开始那层代理可能把客户端IP记成了unknown所以要从前往后找第一个合法的。第二isValidIp里对IPv6的处理是宽松的。因为IPv6地址格式复杂正则写死容易出错。我的策略是如果包含冒号就默认它是合法的IPv6先放行但后续解析省市时发现是IPv6就跳过。如果你只想支持IPv4可以直接把IPv6地址丢弃返回getRemoteAddr()的结果。第三最后的LOCALHOST_IPV6映射很关键。当用户在本机调试时getRemoteAddr()返回的是0:0:0:0:0:0:0:1这就是IPv6格式的127.0.0.1。如果不做转换后续解析IP库时查不到任何信息白白浪费时间。4.3 解析服务的完整实现解析服务是核心我把离线库和在线API串起来做成了一条解析链import org.springframework.stereotype.Service; Service public class IpRegionService { private static final long CACHE_EXPIRE_HOURS 24; private final MapString, RegionInfo cache new ConcurrentHashMap(); public RegionInfo getRegion(String ip) { if (ip null || ip.isEmpty()) { return RegionInfo.unknown(ip null ? : ip); } // 1. 先查内存缓存 RegionInfo cached cache.get(ip); if (cached ! null) { return cached; } RegionInfo regionInfo null; // 2. 查离线库 String result IpRegionUtil.region(ip); if (result ! null !isInvalidOfflineResult(result)) { regionInfo parseOfflineResult(ip, result); } // 3. 离线库没命中或结果异常走在线API if (regionInfo null || regionInfo.isUnknown()) { String[] onlineResult IpApiUtil.getRegionByOnline(ip); if (onlineResult ! null) { regionInfo new RegionInfo(ip, 中国, onlineResult[0], onlineResult[1], ); } } // 4. 仍然查不到标记为未知 if (regionInfo null) { regionInfo RegionInfo.unknown(ip); } // 5. 写缓存注意只缓存有效的、并且不是内网IP的查询结果 if (!isPrivateIp(ip) regionInfo ! null) { cache.put(ip, regionInfo); } return regionInfo; } private boolean isInvalidOfflineResult(String result) { return result null || result.contains(内网IP) || result.contains(0) result.endsWith(内网IP); } private RegionInfo parseOfflineResult(String ip, String result) { String[] parts result.split(\\|); if (parts.length 5) { return RegionInfo.unknown(ip); } String country parts[0]; String province parts[2]; String city parts[3]; String isp parts[4]; if (0.equals(province)) { province 未知; } if (0.equals(city)) { city 未知; } return new RegionInfo(ip, country, province, city, isp); } private boolean isPrivateIp(String ip) { if (ip.startsWith(10.) || ip.startsWith(192.168.) || ip.startsWith(172.) || ip.startsWith(127.)) { return true; } return 127.0.0.1.equals(ip) || localhost.equals(ip); } }这段代码有几个细节用了ConcurrentHashMap做本地缓存不引入额外的缓存框架简单够用。如果项目里已经用了Redis或者Caffeine可以替换成统一缓存。注意缓存Key是IPValue是RegionInfo对象。对一个中低并发的业务系统来说单机内存缓存就足够了。缓存只缓存成功的、不是内网IP的查询结果。内网IP每次查出来的结果都一样但缓存反而可能占内存而且内网IP就没有必要调在线API了所以加了isPrivateIp过滤。离线库解析结果里省份和城市字段如果为0表示缺失要单独处理成“未知”。这个细节不处理的话后面数据库里会存一堆0字符串。4.4 Spring MVC接口层接口层很简单入参是HttpServletRequest内部走上面的IpRegionServiceimport org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; RestController public class IpRegionController { private final IpRegionService ipRegionService; public IpRegionController(IpRegionService ipRegionService) { this.ipRegionService ipRegionService; } GetMapping(/api/ip/region) public RegionInfo region(HttpServletRequest request) { String ip IpUtil.getIpAddr(request); return ipRegionService.getRegion(ip); } }如果项目里没有用Spring用Servlet原生的HttpServlet也可以逻辑完全一样只是把request对象换个来源而已。4.5 数据库表设计数据库表设计我的建议是不要过度设计。根据业务需求来最简单的做法就是在用户表上直接加province和city两个字段用户登录或者下单时顺便解析一次存进去。如果要分析所有用户的访问地域分布那建一张访问日志表更合适CREATE TABLE user_ip_region ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL COMMENT 用户ID, ip VARCHAR(64) NOT NULL COMMENT 请求IP, country VARCHAR(64) DEFAULT COMMENT 国家, province VARCHAR(64) DEFAULT COMMENT 省份, city VARCHAR(64) DEFAULT COMMENT 城市, isp VARCHAR(64) DEFAULT COMMENT 运营商, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, KEY idx_user_id (user_id), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户IP地域信息表;需要注意ip字段用VARCHAR(64)而不是VARCHAR(15)因为要考虑IPv6的存储IPv6地址最长是39个字符。我见过有人把IP字段设计成VARCHAR(15)结果线上存IPv6地址直接报数据过长白踩一个坑。4.6 Nginx侧需要配合的配置光改Java代码还不够Nginx如果不配合你的X-Real-IP永远是空的。Nginx反向代理配置里要加上这几行location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }$remote_addr在Nginx里就是建立TCP连接的那个IP也就是用户的真实IP如果前面没有CDN的话。$proxy_add_x_forwarded_for会在原X-Forwarded-For基础上追加$remote_addr这样应用层拿到的X-Forwarded-For里就会包含完整的链路信息。如果架构里还有CDN这一层那你的服务器看到的$remote_addr就是CDN节点的IP了这时候CDN提供商一般会把用户真实IP放在某个自定义头里比如阿里云CDN用X-Forwarded-ForCloudflare用CF-Connecting-IP需要按服务商的文档来适配。注意从网上抄Nginx配置的时候一定要确认X-Real-IP是设置了还是只是转发了。我看到有些现成配置里有两行proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $http_x_real_ip;第二种写法等于信任了客户端传入的原始X-Real-IP头存在IP伪造风险。正确做法是只用$remote_addr并且宁可让应用层多取几层头也别信任客户端传入的头。5. 常见问题与排查实录5.1 安全问题X-Forwarded-For伪造与IP白名单拦截这个是我踩过最狠的坑。项目刚上线时我用X-Forwarded-For来做一个内部接口的访问控制只允许公司办公网IP段访问。结果上线第二天就有同事反馈说在家里也能访问内部接口而且后台日志显示他的访问IP是公司IP。排查后发现问题出在我信任了客户端传入的X-Forwarded-For头。攻击者这里只是个钻空子的同事在请求里手动加了一个X-Forwarded-For: 公司IP我的代码直接取第一个值就绕过了IP白名单校验。正确的处理方式是应用层不直接信任任何客户端可伪造的头优先取X-Real-IP由可信Nginx写入并且Nginx层用proxy_set_header X-Real-IP $remote_addr强制覆盖。如果你确实需要读X-Forwarded-For也要从右往左找取最后一个由可信代理追加的IP而不是从头开始找第一个。具体实现可以参考Snippet里的逻辑。5.2 性能问题Searcher每次new导致接口超时有段时间线上接口偶尔会出现1秒以上的超时告警查下来发现是同事在代码里每次请求都new了一个新的Searcher对象。ip2region的Searcher构造函数会加载并解析整个xdb文件耗时在几十到几百毫秒。在并发量上来时频繁创建对象直接拖垮了接口。解决方法是把Searcher作为单例初始化一次后面所有线程共用。需要注意Searcher对象本身是线程安全的官方文档明确说了多线程安全。另外如果担心内存占用11MB的xdb文件加载到内存里其实是占了约11MB可以在低内存环境用Searcher.newWithFileOnly()走文件IO但性能会下降一个量级基本不推荐。5.3 上线后IP解析结果不准确的排查思路遇到“解析出的省市不准”的问题时别急着换数据源先按下面几步排查确认你拿到的是不是真实IP。很多“解析不准”其实是拿到了代理IP、CDN IP或NAT网关IP导致的。可以先在服务器上用curl ifconfig.me看出口IP再用浏览器访问一个返回IP的接口比如https://myip.ipip.net对比看看你拿到的到底是谁的IP。确认IP库版本是否太旧。ip2region的数据文件基本是定期更新的。如果业务对准确率要求高建议每半年更新一次xdb文件。确认离线库是否命中了保留地址段。比如运营商级NATCGNAT地址100.64.0.0/10在旧的IP库里可能查不到信息需要单独处理。我遇到过的一个真实案例用户投诉说IP定位到了隔壁省追踪后发现用户的网络走的是运营商级NAT出口IP是省级出口的IP不是本市IP。这种情况下任何IP定位方案都无能为力因为用户根本拿不到独立的公网IP。给产品解释清楚这个原理他们通常能理解。5.4 在线API超时与限流的处理策略淘宝IP库虽然免费但稳定性并不好。我实测过一段时间接口偶发超时单IP的QPS限制也比较严格。在线上代码里不能直接同步阻塞调用这个接口否则第三方抖动会直接影响你的业务接口。我的策略是加三层防护第一层HttpClient设置连接超时和读取超时都是3秒。超时后直接返回null走“未知”分支不阻塞主流程。第二层加内存缓存。同一个IP一天之内只查一次外部接口之后的请求直接读缓存。这能挡住绝大多数流量。第三层降级处理。如果解析失败或者在线API也查不到就返回“未知省份”不抛异常不影响用户正常请求。如果你有更严格的可用性要求可以把在线IP查询做成异步的。比如用户登录时先返回“未知”后台异步解析IP并更新数据库下次请求就能查到准确省市了。这个方案在用户第一次请求时少一点信息但换来了接口的稳定性和低延迟。5.5 本地调试时的特殊处理本地跑项目时大家的IP基本都是127.0.0.1或localhost。这时解析出来的结果要么是“内网IP”要么是请求远程API返回空。所以建议在调试代码里加一行判断if (isPrivateIp(ip)) { return new RegionInfo(ip, 本地, 本地, 本地, 本地); }一是日志清晰二是避免本地调试时频繁调用在线API浪费配额。还见过一个同事在测试环境用127.0.0.1调淘宝IP库结果对方返回了一个固定结果把他绕晕了好一阵。6. 踩坑清单与扩展思路做一个Java获取IP并解析省市的功能核心代码可能就几十行但真正上线后踩的坑汇总一下还挺多。我建议把下面这张速查表存下来排查问题时照着过一遍。问题现象根本原因解决方案所有用户IP都一样未处理代理转发Nginx配置X-Real-IP应用层优先读取IP解析出“内网IP”拿到的是代理内网IP检查Nginx配置确认$remote_addr取到的是用户IPIP经常解析到邻近省份运营商NAT出口无法根除向业务方说明原理离线库解析全是“未知”xdb文件版本过旧更新ip2region数据文件接口偶发超时在线API慢加超时控制用缓存降低外部依赖用户篡改IP直接信任请求头只在Nginx层写IP应用层不要信任客户端头IPv6进来解析失败数据源不支持对IPv6单独判断或走在线API的IPv6支持做完核心功能后还可以往下面几个方向扩展如果把解析结果对接到日志系统比如ELK你就能做实时的大屏地域分布图销售和运营都爱看这种可视化数据。如果用在安全风控上可以把“IP归属地”跟“用户常用登录地”做比对异地登录时触发二次验证。这些都是现成的、能提升项目价值感的扩展点而且代码上都不用动太多就是在解析完IP后多接一根管道的事。我在实际开发中还有一个体会这种看似很小的工具功能恰恰是面试官最爱问的“高频八股”。Java基础、HTTP协议、反向代理、数据结构、异常处理、缓存设计全都融在一个小功能里。与其去背面试题不如自己动手把这块代码完整实现一遍踩过的坑比背十道面试题都好使。
返回列表