🤖
AI审核中

一个IP查询接口,暴露出的系统设计边界

Java 10分钟 117浏览 0评论

用户提交评论时,系统根据 IP 查询省、市、区,再把地点展示在评论旁边。功能看似简单,却会让原本只依赖数据库的写操作,额外依赖第三方网络、接口额度和响应格式。

如果定位接口超时,评论还能不能提交?接口返回成功但地点为空,数据库应该保存什么?客户端伪造 X-Forwarded-For,系统会不会记录错误位置?

这些问题可以归结为一个原则:

IP 归属地只是增强数据,定位失败不能升级为评论提交失败。

一、先划分核心数据与增强数据

评论正文、文章 ID、评论者和创建时间属于核心数据;IP 归属地只是展示增强项。因此业务契约应当明确:

评论合法 + 定位成功 -> 保存真实地区
评论合法 + 定位失败 -> 保存“IP未知”
评论本身不合法     -> 拒绝保存
数据库写入失败     -> 返回提交失败
flowchart LR
    A["评论请求"] --> B["提取客户端 IP"]
    B --> C["调用定位接口"]
    C -->|"有效地点"| D["保存真实地区"]
    C -->|"失败或空结果"| E["保存 IP未知"]
    D --> F["保存评论"]
    E --> F

降级只作用于地区字段,不能掩盖评论校验或数据库错误。

二、把不同故障统一成稳定业务状态

外部定位可能因缺少配置、连接超时、HTTP 非成功、非法 JSON、业务码错误或地点为空而失败。对监控而言,这些原因需要分别统计;对评论展示而言,可以统一为:

IP未知

当前转换规则可以简化为:

default String buildLocationString(IpLocationResult result) {
    if (result != null && result.getCode() == 200) {
        String location = result.getFullLocation();
        if (location != null && !location.isEmpty()) {
            return result.isChinaIp()
                    ? location
                    : result.getFullLocationWithCountry();
        }
    }
    return "IP未知";
}

这里同时检查对象、业务码和地点内容。即使 HTTP 请求成功,只要最终没有有效省市区,也不能保存空字符串或伪造地点。

统一哨兵值比 null"""0" 混用更稳定:前端展示、数据库查询和失败率统计都只需要处理一种状态。

三、异常被兜底,不等于真正非阻塞

当前链路在保存评论前同步调用定位接口:

String ip = IpParseUtil.getIpAddr(request);
IpLocationResult result = IpParseUtil.parseByApi(ip);
comments.setProvince(buildLocationString(result));

接口抛出异常时,工具会捕获异常并返回 null,评论最终保存为 IP未知。这证明“定位失败后仍可继续”,但外部请求仍占用当前 Servlet 线程。

如果 HTTP 客户端没有明确超时,第三方不需要抛异常,只要迟迟不响应,就可能拖慢评论提交并耗尽 Web 线程池。

因此,IP 定位应该使用独立的短超时客户端:

@Bean("ipLocationRestTemplate")
public RestTemplate ipLocationRestTemplate() {
    SimpleClientHttpRequestFactory factory =
            new SimpleClientHttpRequestFactory();
    factory.setConnectTimeout(500);
    factory.setReadTimeout(800);
    return new RestTemplate(factory);
}

示例数值不是通用答案,重点是给增强调用设置独立预算,不能让它无限侵占核心写入时间。当前默认 RestTemplate 尚未体现这条上限,因此这是需要补强的边界。

四、需要更低延迟时,改为异步补全

如果不希望第三方延迟进入用户请求,可以先保存评论:

1. 保存评论,地区初始为“IP未知”
2. 返回评论 ID
3. 事务提交后发布定位任务
4. 后台查询成功后更新地区

异步方案能缩短前台等待时间,但需要处理任务幂等、失败重试、进程重启丢任务和最终一致性。低流量博客使用“短超时同步调用”通常更简单;高并发写入再考虑持久队列或 outbox。

五、不要无条件相信转发 IP 头

很多工具依次读取 X-Forwarded-ForX-Real-IPrequest.getRemoteAddr(),遇到多个地址就取第一个。当前工具也采用了类似方式。

问题是请求头可以被客户端伪造。如果应用端口允许公网直连,攻击者可以自行提交:

X-Forwarded-For: 8.8.8.8

系统就可能查询并保存虚假地点。正确做法是:

  • 应用只接受可信反向代理转发的业务流量;
  • 代理删除客户端原有转发头,再写入自己的值;
  • 应用按可信代理链解析地址;
  • 校验 IPv4、IPv6,并拒绝非法、私有和保留地址。

“读取到了一个 IP”不代表“这个 IP 值得信任”。

六、未知比伪造地点更诚实

为了方便本地演示,当前工具会把部分回环地址替换为固定公网测试 IP。这种逻辑进入生产后会制造假数据:本地请求或代理配置错误会被记录成同一个真实地区。

更安全的规则是:

测试 profile + 显式配置 -> 可以使用测试 IP
正常运行 + 私有/回环地址 -> 直接返回“IP未知”

IP 地址还涉及隐私。当前定位日志会记录原始 IP,生产环境应考虑脱敏、缩短保留周期,并避免记录包含 API Key 的完整请求 URL。

七、移除本地 IP 库要检查四层

从本地 ip2region 切换为远程接口时,只删除 Java 调用并不完整。还需要确认:

源码层 -> 无 org.lionsoulSearcherxdb 读取代码
依赖层 -> dependency:tree 不再包含 ip2region
资源层 -> 仓库和模块 JAR 中没有 .xdb
交付层 -> 最终聚合 JAR 及内嵌 Blog JAR 均无残留

本项目已对这四层完成检查。移除本地 fallback 后,依赖和打包更简单,但系统更加依赖外部网络,因此短超时、失败归一化和监控必须同步建立。

八、测试重点应放在失败路径

除了成功拼接省市区,还应覆盖:

场景 预期结果
定位接口抛异常 评论仍可构造,地区为 IP未知
业务码非成功 地区为 IP未知
成功响应但地点为空 地区为 IP未知
国内地址完整 拼接省市区
国外地址完整 添加国家前缀
私有或回环地址 不调用远端
伪造转发头 不绕过可信代理规则
读取超时 在预算内返回 IP未知

当前评论转换测试覆盖了接口异常、国内地点、成功空结果和评论内容保持,4 项测试全部通过。超时、可信代理和私有地址仍应在后续改造中补充测试。

九、总结

IP 定位只是一个 HTTP GET,却跨越了网络可靠性、数据契约、代理信任和隐私边界。

可靠的实现应做到:

  • 评论是核心事实,地区只是增强数据;
  • 缺配置、异常、非成功和空结果统一保存 IP未知
  • 定位客户端具有独立且很短的超时预算;
  • 只有在可信代理边界内才接受转发 IP 头;
  • 私有和回环地址返回未知,不伪装成公网地点;
  • 用户展示统一未知状态,内部监控保留具体失败原因;
  • 移除本地 IP 库时检查源码、依赖、资源和最终 JAR。

一句话概括:

外部增强可以失败,但等待时间必须有上限,最终数据必须诚实。

当定位服务不可用时,用户仍能提交评论并看到“IP未知”;与此同时,系统内部能够准确识别失败原因。这才是外部数据增强应有的失效安全设计。

0 条评论
如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧☕
微信
支付宝
  0 条评论