反向代理

获取用户真实 IP 地址

为了实现反向代理,nginx 增加了 ngx_http_proxy_module 模块。配置文件中的 proxy_set_header 指令即属于该模块,用于 重写 从 nginx 转出的请求头。

配置方法

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;

分析

image-center

Remote Address

Remote Address 是 nginx 与直接访问它的设备进行 TCP 连接过程中,获得的对方真实地址。如果对方是客户端,则得到的地址即客户端的真实 IP 地址,如果对方也是一个代理,则得到的是代理的 IP 地址。

无法伪造,因为建立 TCP 连接需要三次握手,如果伪造了源 IP,无法建立 TCP 连接,更不会有后面的 HTTP 请求。

不同语言获取 Remote Address 的方式不一样,例如 php 是 $_SERVER["REMOTE_ADDR"],Node.js 是 req.connection.remoteAddress,但原理都一样。

在本配置中,Remote Address 用变量 $remote_addr 来表示,把它导出给后端的服务器查看。

X-Real-IP 字段

X-Real-IP 字段是一个自定义头部字段。X-Real-IP 通常被 HTTP 代理用来表示与它产生 TCP 连接的设备 IP,这个设备可能是其他代理,也可能是真正的请求端。需要注意的是,X-Real-IP 目前并不属于任何标准,代理和 Web 应用之间可以约定用任何自定义头来传递这个信息。

我们的配置是把真实的客户端地址 $remote_addr 赋值给 X-Real-IP。注意,如果客户端是经过 CDN 或其他反向代理连接过来的,那么这个 $remote_addr 地址就是 CDN 或反代的地址了。

X-Forward-For 字段

X-Forwarded-For 是一个 HTTP 扩展头部。HTTP/1.1(RFC 2616)协议并没有对它的定义,它最开始是由 Squid 这个缓存代理软件引入,用来表示 HTTP 请求端真实 IP。如今它已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中。

X-Forwarded-For 请求头格式非常简单:

X-Forwarded-For: client, proxy1, proxy2

可以看到,XFF 的内容由多个部分组成,用「英文逗号 + 空格」分隔,最开始的是离服务端 最远 的设备 IP 地址,然后是每一级代理设备的 IP 地址。

image-center

假设用户真实 IP 地址为 IP0,如果一个 HTTP 请求到达服务器之前,经过了三个代理 Proxy1、Proxy2、Proxy3,它们的 IP 地址分别为 IP1、IP2、IP3,那么按照 XFF 标准,Web 服务端最终会收到就是:

X-Forwarded-For: IP0, IP1, IP2

Proxy3 直连服务器,它会给 XFF 追加 IP2,表示它是在帮 Proxy2 转发请求。列表中并没有 IP3,IP3 可以在服务端通过 Remote Address 字段获得。我们知道 HTTP 连接基于 TCP 连接,HTTP 协议中没有 IP 的概念,Remote Address 来自 TCP 连接,表示与服务端建立 TCP 连接的设备 IP 地址,在这个例子里就是 IP3。

变量 $proxy_add_x_forwarded_for

变量 $proxy_add_x_forwarded_for 等效于 $http_x_forwarded_for,$remote_addr,它会把 nginx 反向代理本机的 IP 地址 追加X-Forwarded-For 字段中。

例:如果上一层连过来的是 CDN,它设置了 X-Forwarded-For(通常 CDN 会隐藏自己的 IP 地址,只保留客户端的),到 nginx 这里又设置了一次,追加一个 $proxy_add_x_forwarded_for,则 X-Forwarded-For 字段的内容变成 客户端地址, nginx 地址。所以,如果知道 CDN 设置了 X-Forwarded-For,且只有客户端真实地址的话,nginx 就可以不设置该字段了,默认即可。

Host 字段

HTTP 请求头中的 Host 字段表示所请求的 目的主机名

如果后端 web 服务器使用了类似防盗链功能,或者根据 HTTP header 中的 Host 字段来进行路由或过滤功能的话,nginx 必须重写请求头中的 Host 字段,否则会导致请求失败。

为什么不用 $http_host 而用 $host:

变量 $http_host 与 $host 同样都能表示请求头中的 Host 字段,为什么不用前者?

如果 Host 字段没有出现在 HTTP 头中,$http_host 的变量值将为 ,而 $host 却会使用实际处理的虚拟主机的 server_name 做为变量值。因此通常会用 $host 来代替 $http_host 变量,从而避免 http 请求中丢失 Host 头部的情况下,Host 字段不被重写的失误。

小结
  1. Web 服务器在进行与安全有关的操作时,只能通过 Remote Address 获取 IP,不能相信任何请求头;
  2. Nginx 做反向代理时,需要用 X-Forwarded-For 最后一节 或 X-Real-IP 来获取客户端 IP 地址,因为此时 Remote Address 得到的是 Nginx 的内网 IP 地址,同时还应禁止 Web 服务器直接对外提供服务;
  3. 在与安全无关的场景,可以从 X-Forwarded-For 靠前的位置获取 IP,但是需要校验 IP 格式合法性,避免使用用户伪造的内容;

网上有些文章建议这样配置 Nginx,其实并不合理:

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;

这样配置之后,安全性确实提高了,但是也导致请求到达 Nginx 之前的所有代理信息都被抹掉,无法为真正使用代理的用户提供更好的服务。通过代理连接到 nginx 反向代理服务器上的用户,其原始地址将无法获取。