一次登录限流故障排查:为什么 request.getRemoteAddr() 会突然变成内网 IP?
一次看似普通的登录限流故障,却暴露了生产环境中容易被忽略的客户端 IP 获取问题。本文结合真实案例,分析 `request.getRemoteAddr()` 为什么会返回 Docker 容器内网 IP,介绍可信代理(Trusted Proxy)的设计原理,以及 `X-Forwarded-For` 的正确解析方式。
一、事故背景
前几天,我对登录接口增加了一套限流策略,并顺利发布上线。
本以为一切正常,没想到下午突然收到多个同事反馈:
登录不了,系统一直提示"失败次数过多"。
最先发现问题的是一位负责注册功能优化的同事。
由于我们的小程序注册流程会先调用登录接口判断用户是否已存在,因此他在测试注册功能时,一直卡在登录阶段,误以为是自己刚上线的代码导致的问题,排查了很久才发现真正失败的是登录接口。
就在大家一起分析问题时,有同事提到了一句:
今天有人上线了客户端 IP 获取逻辑相关的优化。
这条信息一下引起了我的注意。
因为登录限流正是基于客户端 IP 进行统计,如果 IP 获取出现异常,就可能导致大量用户被统计到同一个限流桶中。
于是,排查重点转向了客户端 IP 获取逻辑。
二、排查过程
第一反应:是不是前端重复请求?
虽然怀疑 IP 获取逻辑,但还是先确认是不是前端导致了请求异常。
于是抓包查看整个登录流程。
结果发现:
前端只发送了一次登录请求。
不存在按钮重复点击,也不存在轮询调用。
前端可以排除。
第二个怀疑:是不是限流参数配置有问题?
继续查看限流逻辑。
系统同时做了:
-
用户维度限流
-
IP 维度限流
阈值配置也比较正常,不可能一次请求就触发限流。
说明问题不是限流配置,而是统计的数据出了问题。
先止血,再继续排查
线上业务不能一直受影响。
为了尽快恢复业务,我先进行了临时处理:
-
清理 Redis 中已经累计的失败次数
-
将限流参数改成支持配置中心动态调整,方便后续快速修改
-
针对部分登录场景增加降级处理,避免异常情况下直接进入限流
业务恢复后,再继续分析真正原因。
一个奇怪的内网 IP
继续查看 Redis 中的限流统计数据时,我发现了一个异常现象。
大量失败请求,都来自同一个客户端 IP:
172.16.x.x
第一眼看到时,我其实愣了一下。
这是一个 Docker 内网地址。
更奇怪的是:
它并不是所有请求都有,而是只有部分请求才会出现。
如果 IP 获取逻辑存在 Bug,那理论上所有请求都应该拿到这个地址。
为什么偏偏只有一部分?
这说明,问题很可能和部署架构有关。
三、真正的原因
继续检查部署结构之后,终于找到了答案。
Auth 服务采用了多实例部署。
其中,部分实例与 Nginx 部署在同一台宿主机,Nginx 又运行在 Docker Bridge 网络中。
整个请求路径大致如下:
┌──── 多个 Auth 实例 ────┐
用户 → Nginx(Docker) ─┤ │
│ 大部分实例:正常解析 IP
│ │
│ 少数同机实例:返回 Docker 内网 IP
└────────────────────────┘问题只发生在少数同机部署的实例上。
正常实例
请求经过网络转发。
request.getRemoteAddr() 获取到的是可信代理地址。
程序继续解析 X-Forwarded-For。
最终得到真实客户端 IP。
整个限流流程正常。
异常实例
问题出在与 Nginx 同机部署的实例。
由于 Docker Bridge 网络的通信方式,应用拿到的 TCP 对端地址变成了 Docker 容器自己的地址,例如:
172.16.x.x
而这个地址没有配置到可信代理白名单中。
程序认为:
当前请求并不是来自可信代理。
于是直接返回:
request.getRemoteAddr()最终所有经过该实例的请求,都共享了同一个"客户端 IP"。
IP 维度限流自然瞬间被触发。
这也解释了为什么:
只有部分用户受到影响。
因为是否命中异常节点,完全取决于负载均衡的结果。
四、问题根因:为什么不能直接读取 X-Forwarded-For?
很多人看到这里都会有一个疑问:
既然 Nginx 已经写入了 X-Forwarded-For,
为什么不直接读取?
答案只有一个:
因为客户端可以伪造。
例如:
GET /login
X-Forwarded-For: 8.8.8.8如果程序无条件相信这个 Header,攻击者就可以:
-
绕过 IP 限流
-
绕过黑名单
-
绕过风控策略
-
污染日志数据
因此,一个安全的客户端 IP 获取流程必须遵循一个原则:
只有 TCP 连接本身来自可信代理时,才解析 X-Forwarded-For。
否则,一律使用 request.getRemoteAddr()。
五、为什么要从右往左解析 X-Forwarded-For?
假设 Header 如下:
223.5.5.5,10.0.0.1,172.16.x.x
其中:
客户端
↓
223.5.5.5
↓
代理1
10.0.0.1
↓
Nginx
172.16.x.x
很多代码都会直接取最左边。
实际上这是不安全的。
因为客户端完全可以发送:
8.8.8.8,9.9.9.9
真正可信的是:
离应用最近的代理。
因此业内普遍采用:
从右向左遍历代理链。
不断跳过可信代理。
直到遇到第一个非可信地址。
那个地址,才是真正的客户端 IP。
六、本次故障中的执行过程
当异常请求到达应用时,代码执行流程实际上如下:
remoteAddr = "172.16.x.x"
↓
isTrustedProxy(remoteAddr)
↓
false
↓
直接返回 remoteAddr
↓
客户端 IP = 172.16.x.x
↓
大量请求共享同一客户端 IP
↓
IP 限流被瞬间触发整个流程其实完全符合程序设计。
真正的问题在于:
Docker 网络中的代理地址,没有被纳入可信代理范围。
七、修复方案
临时修复
将 Docker 网络中的代理地址加入可信代理配置:
trusted:
proxies: 172.16.x.x如果存在多个代理:
trusted:
proxies: 172.16.x.x,172.16.x.y,10.x.x.x更推荐的方案
如果 Docker 地址可能变化,更建议支持 CIDR 网段配置,例如:
trusted:
proxies:
- 172.16.0.0/16避免容器重建后需要再次修改配置。
同时建议增加以下监控:
-
同一 IP 短时间大量触发限流
-
客户端来源出现大量内网地址
-
Trusted Proxy 配置变更
-
Docker 网络异常变化
这样能够更早发现类似问题。
八、总结
这次故障看起来只是一次登录限流异常,但真正暴露出来的,其实是客户端 IP 获取与部署架构之间的耦合问题。
很多开发者认为:
request.getRemoteAddr()返回的就是用户真实 IP。
实际上并不是。
它返回的是:
当前 TCP 连接对端的地址。
这个地址可能是:
-
用户
-
Nginx
-
Docker Bridge 网络
-
四层负载均衡
-
七层代理
-
其他反向代理
真正的客户端 IP,需要结合可信代理和 X-Forwarded-For 一起解析。
因此,一个可靠的客户端 IP 获取方案至少应该做到:
-
不直接信任
X-Forwarded-For -
建立可信代理(Trusted Proxy)机制
-
从右向左解析代理链
-
支持 CIDR 网段配置,而不仅仅是单个 IP
-
对异常内网来源建立监控与告警
很多线上故障,并不是代码写错了,而是代码运行的环境发生了变化。
同样一段 request.getRemoteAddr(),在不同的网络拓扑下,返回的可能是完全不同的结果。这也是生产环境中最容易踩坑、却最容易被忽略的一类问题。