首页
学习
活动
专区
圈层
工具
发布

SpringBoot 中获取真实客户端 IP 的终极方案,99% 的人都没做对!

线上日志里突然冒出来一堆:

login failed, ip=127.0.0.1

login failed, ip=10.20.4.17

login failed, ip=10.20.4.17

一开始我还以为登录接口的 IP 记录写错了。

翻代码一看:

String ip = request.getRemoteAddr();

代码没毛病。

问题出在前面还有 Nginx、网关、负载均衡。SpringBoot 看到的“客户端”,压根不是用户浏览器,而是最后一跳代理。

这种问题最容易被写成一个IpUtils,然后疯狂判断请求头:

X-Forwarded-For

X-Real-IP

Proxy-Client-IP

WL-Proxy-Client-IP

HTTP_CLIENT_IP

...

我现在看到这种代码第一反应不是佩服兼容性,而是先问一句:

这些 Header,你凭什么信?

因为 Header 是客户端能伪造的。

比如接口直接允许公网访问,我自己发:

curl http://api.example.com/login \

-H "X-Forwarded-For: 8.8.8.8"

你的 Java 如果无脑取X-Forwarded-For第一个 IP,日志里可能真就记成了8.8.8.8。

拿这个 IP 做审计、风控、黑名单,后面基本都白干。

Spring 官方文档也专门提醒过这个问题:应用本身无法判断Forwarded、X-Forwarded-*到底是可信代理加的,还是恶意客户端塞进来的,所以信任边界上的代理应该先清理外部传入的这些 Header。

这才是获取真实 IP 最容易漏掉的一层。

我比较习惯把链路先画出来:

客户端

|

v

负载均衡 / Nginx

|

v

SpringBoot

如果前面只有一层 Nginx,我反而不喜欢搞得太复杂。

Nginx 入口直接重写:

location / {

  proxy_pass http://order-service;

  proxy_set_header Host $host;

  proxy_set_header X-Real-IP $remote_addr;

  proxy_set_header X-Forwarded-For $remote_addr;

  proxy_set_header X-Forwarded-Proto $scheme;

}

注意这里我没有直接用网上特别常见的:

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

不是说它不能用。

而是如果这是公网信任边界,我得先确认前面到底还有没有可信代理。否则客户端自己带一个X-Forwarded-For进来,再一路追加,Java 端取“第一个 IP”,这个洞还是没堵上。

如果链路是:

客户端 -> SLB -> Nginx -> SpringBoot

那就不能粗暴覆盖了。

这时候应该明确:谁是可信代理、哪一层负责清洗 Header、哪一层负责追加代理地址。

这东西必须跟实际网络拓扑一起配,脱离代理链单独写一个万能getClientIp(),我一般不信。

SpringBoot 这边也别急着自己解析字符串。

可以让容器处理转发头:

server:

forward-headers-strategy: native

Spring Boot 当前文档明确说明,如果代理提供常见的X-Forwarded-For和X-Forwarded-Proto,可以使用NATIVE,交给底层 Web Server 原生处理。

Tomcat 背后实际上有RemoteIpValve这一套机制,它会根据代理头和可信代理规则重新确定请求的 remote address。

这样业务代码反而干净:

@Component

public class RequestSource {

  public String clientIp(HttpServletRequest request) {

      String remote = request.getRemoteAddr();

      if (remote == null || remote.isBlank()) {

          return "unknown";

      }

      int scope = remote.indexOf('%');

      return scope > 0 ? remote.substring(0, scope) : remote;

  }

}

Controller 里直接用:

@PostMapping("/session")

public LoginResult login(HttpServletRequest request,

                       @RequestBody LoginCommand command) {

  String clientIp = requestSource.clientIp(request);

  log.info("login request, account={}, clientIp={}",

          command.account(), clientIp);

  return loginService.execute(command, clientIp);

}

有人可能会问,那我直接这样不行吗?

request.getHeader("X-Forwarded-For").split(",")[0]

能跑。

但我不会把它作为生产环境的最终方案。

假设 Header 是:

X-Forwarded-For: 203.0.113.7, 10.10.2.11, 10.10.5.23

这里至少有三个问题:

客户端能不能伪造第一段?

10.10.2.11是不是公司网关?

10.10.5.23是不是 Nginx?

只看字符串根本回答不了。

所以我排这种问题时,顺序一般是:

先看 request.getRemoteAddr()

确认 SpringBoot 前面到底有几层代理

抓 Nginx / 网关 access log

确认 X-Forwarded-For 实际内容

确定可信代理范围

入口清洗 Header

最后才配置 SpringBoot

不是上来写二十行 Java 判断 Header。

还有一个坑得顺手堵掉:SpringBoot 服务端口最好不要绕过网关直接暴露公网。

否则你前面 Nginx 清洗得再漂亮,攻击者直接请求 8080:

公网 -> SpringBoot:8080

整个信任模型就没了。

防火墙、安全组或者 Kubernetes NetworkPolicy 至少要保证应用只能接收可信入口的流量。

所以这事真正靠谱的方案,不是找到一个“最全的 IP Header”。

而是把这三件事做对:

代理入口清洗伪造 Header,SpringBoot 只信任确定的代理链,业务代码最终只拿处理后的getRemoteAddr()。

真实 IP 从来不是一行 Java 代码的问题。

只要中间多了一层代理,它首先就是个信任问题。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OVwEXLFyhKFB4h5KA64VgvJw0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券