> ## Content Index
> Fetch the complete content index at: https://blog.20061124.xyz/llms.txt
> Use this file to discover other available public pages before exploring further.

# Nginx 反代后拿不到真实 IP
- URL: https://blog.20061124.xyz/nginx-real-ip/
- Published: 2026-09-29T02:00:00.000Z
- Updated: 2026-10-01T07:52:08.000Z
- Description: 访问日志里所有访客都变成了 127.0.0.1。要让真实 IP 走完这一路，代理层和应用层各有一件事必须做。
- Author: Ray Jack
- Tags: 运维

给博客加了一个访问统计的小脚本，跑了两天，发现一件怪事：所有访客的 IP 都是 `127.0.0.1`。

不是「大部分是」，是「全部都是」。连我自己从家里访问，记录里也是本机地址。这说明拿到的根本不是访客的 IP，而是某一层代理自己的地址。

## 一、先确认在哪一层丢的

这套结构很常见：Nginx 监听 443 做反向代理，把请求转发给本机 `127.0.0.1:2368` 上的后端程序。我在两个位置分别打日志：

```
Nginx 访问日志        →  正常，是真实公网 IP
后端拿到的 remote_addr →  127.0.0.1

```

既然 Nginx 自己是知道真实 IP 的，那问题就只在「它转发的时有没有告诉后端」。

## 二、根因：默认配置不会透传客户端地址

一开始我下意识地认为反代会自动带上原始 IP，其实不会。TCP 是点对点的：Nginx 和后端之间**新开了一条连接**，在这条连接里，客户端就是 Nginx 自己。

如果不显式加请求头，后端看到的源地址只能是 `127.0.0.1`。真实 IP 想传过去，只能靠**在 HTTP 头里捎带**，而这件事 Nginx 默认不做。

```
访客 1.2.3.4  ──►  Nginx  ──►  127.0.0.1:2368
                     │
                     └─ 不加头，后端只知道「Nginx 找我」
```

## 三、加上两个标准头

在 `location` 里补两行，这是最通行的做法：

```
location / {
    proxy_pass http://127.0.0.1:2368;

    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

```

这两个头的作用不太一样，值得分清楚：

- `X-Real-IP` 只放一个地址，就是**直连 Nginx 的那个客户端**的 IP。简单直接，但前面要是还套了一层 CDN，它就是 CDN 的地址。
- `X-Forwarded-For` 是一个**逗号分隔的列表**，每一层代理都会往后追加自己的上游地址，所以它是「一路走过来的完整链条」。

`$proxy_add_x_forwarded_for` 这个变量的含义是：如果请求里已经有 `X-Forwarded-For`，就在它后面接上当前直连方的地址；如果没有，就直接写当前地址。所以链条是自然累加出来的：

```
X-Forwarded-For: 1.2.3.4, 10.0.0.7, 127.0.0.1
                 ↑访客    ↑CDN     ↑本机 Nginx 追加的
```

## 四、加了头，后端还是拿不到

改完配置重载 Nginx，日志里还是 `127.0.0.1`。这一步卡了最久，因为头明明已经发过去了。

用 curl 从外部请求一次，看后端收到的原始头：

```
curl -H 'X-Forwarded-For: 9.9.9.9' http://127.0.0.1:2368/debug

X-Forwarded-For: 9.9.9.9      ← 头确实到了
remote_addr:     127.0.0.1    ← 但变量没变

```

原因是：**HTTP 头只是数据，程序不会自动去读它**。绝大多数框架出于安全考虑，默认*不信任*任何客户端发来的转发头——否则任何人加一个头就能把自己伪装成别的 IP。

所以还有第二步，得在后端侧显式开启信任。Node / Express 里是一行：

```
app.set('trust proxy', true);

```

开了之后，框架才会在计算 `req.ip` 时去读 `X-Forwarded-For`。其他语言/框架也有对应的开关，只是叫法不同（比如 Flask 的 `ProxyFix`、Django 的 `USE_X_FORWARDED_HOST` 系列配置）。

## 五、别忘了它本身可以被伪造

这一步做完 IP 就正常了，但顺手补上安全上的收尾。

`X-Forwarded-For` 是一个普通的请求头，**客户端可以自己伪造**。如果你直接把「链条里第一个地址」当真实 IP，那访客只要发一个 `X-Forwarded-For: 1.1.1.1` 就能污染你的统计、甚至绕过基于 IP 的限流。

所以只应该信任**自己控制的代理**转发过来的那些地址。Nginx 侧可以用 realip 模块把「可信来源」和「取值规则」写死：

```
set_real_ip_from 10.0.0.0/8;      # 只信内网代理
set_real_ip_from 172.16.0.0/12;
real_ip_header    X-Forwarded-For;
real_ip_recursive on;             # 从右往左跳过可信地址，取第一个不可信的

```

开了之后，Nginx 的 `$remote_addr` 自己就变成了真实客户端 IP——上面那段 `proxy_set_header` 也就自动正确了。但注意：**只把你自己那几台代理的网段写进去**，范围开太大会等于没做防护。

## 六、小结

这个问题其实横跨了两层，每次只在其中一层改都不够：

- 代理层负责**把头带上**（`X-Real-IP` / `X-Forwarded-For`），默认配置不会带
- 应用层负责**选择相信这个头**，默认不信任；两件事都做完，IP 才会正确
- `X-Forwarded-For` 是会被追加的列表，取第一个地址在多级代理下才正确，但它可被伪造
- 用 `set_real_ip_from` 把可信网段收窄，是这套方案能不能安全用的前提

以后再看到日志里清一色的 `127.0.0.1`，基本可以直接往「转发头没带」或「后端没开信任」这两个方向查，前面那些环节都是对的。