Nginx中间件玄机随录

nginx 如何配置来获取用户真实IP

2020-03-01  本文已影响0人  RainingMan

本文转载自https://www.cnblogs.com/hftian/p/11127152.html
作者: hftian

##1.背景知识

1.1. 前提知识点:

还有nginx中的几个变量:

1.2.前提与铁律

铁律:当多层代理或使用CDN时,如果代理服务器不把用户的真实IP传递下去,那么业务服务器将永远不可能获取到用户的真实IP。

1.3.用户真实IP的来源和现实情况

首先说用户真实的IP也会存在很多人共用一个IP的情况。用户的请求到达业务服务器会经过以下几种情形:

1.3.1.宽带供应商提供独立IP

比如家里电信宽带上网,电信给分配了公网ip,那么一个请求经过的ip路径如下:

192.168.0.101(用户电脑ip)–>192.168.0.1/116.1.2.3(路由器的局域网ip及路由器得到的电信公网ip)–>119.147.19.234(业务的前端负载均衡服务器)–>192.168.126.127(业务处理服务器)。

这种情况下,119.147.19.234会把得到的116.1.2.3附加到头信息中传给192.168.126.127,因此这种情况下,我们取得的用户ip则为:116.1.2.3。如果119.147.19.234没有把116.1.2.3附加到头信息中传给业务服务器,业务服务器就只能取上上一级的119.147.19.234.

1.3.2.宽带供应商不能提供独立IP

宽带提供商没有足够的公网ip,分配的是个内网ip,比如长宽等小的isp。请求路径则可能为:

192.168.0.123(用户电脑ip)–>192.168.0.1/10.0.1.2(路由器的局域网ip及路由器得到的运营商内网ip)–>211.162.78.1(网络运营商长城宽带的公网ip)–>119.147.19.234(业务的前端负载均衡服务器)–>192.168.126.127(业务处理服务器)。
这种情况下得到的用户ip,就是211.162.78.1。 这种情况下,就可能出现一个ip对应有数十上百个用户的情况了(受运营商提供的代理规模决定,比如可能同时有几千或上万的宽带用户都是从211.162.78.1这个ip对外请求)。

1.3.3.手机2g上网

网络提供商没法直接提供ip给单个用户终端,以中国移动cmwap上网为例,因此请求路径可能为:

手机(手机上没法查看到ip)–> 10.0.0.172(cmwap代理服务器ip)–>10.0.1.2(移动运营商内网ip)–>202.96.75.1(移动运营商的公网ip)–>119.147.19.234(业务的前端负载均衡服务器)–>192.168.126.127(业务处理服务器)。
这种情况下得到的用户ip,就是202.96.75.1。2008年的时候整个广东联通就三个手机上网的公网ip,因此这种情况下,同一ip出现数十万用户也是正常的。

1.3.4.大厂,有几万或数十万员工,但是出口上网ip就一个

这种也会出现来自同一ip的超多用户,比如腾讯、百度等某一个办公区,可能达到几万人,但出口IP可能就那么几个。

2.如何获取用户真实IP

2.1. 当业务服务器直接暴露在公网上,并且未使用CDN和反向代理服务器时:

可以直接使用remote_addr。如 PHP 可以直接使用

<pre data-evernote-id="16" class="js-evernote-checked" style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 1em;">$_SERVER['REMOTE_ADDR'] </pre>

这时候,HTTP_X_FORWARDED_FOR 和 HTTP_X_REAL_IP 都是可以被伪造的,但REMOTE_ADDR是客户端和服务器的握手IP,即client的出口IP,伪造不了。
比如用下面这条命令来请求一个php文件,并且输出$_SERVER信息

<pre data-evernote-id="17" class="js-evernote-checked" style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 1em;">curl http://10.200.21.32/test.php -H 'X-Forwarded-For: unkonw, <alert>aa,11.22.33.44,11</alert>" 1.1.1.1' -H 'X-Real-IP: 2.2.2.2, <a>' </pre>

结果是(只取部分信息,10.100.11.25是我电脑的IP,服务器是内网服务器,所以不会有公网IP)

  1. [HTTP_X_FORWARDED_FOR] => unkonw, <alert>aa,11.22.33.44,11</alert>" 1.1.1.1

  2. [REMOTE_ADDR] => 10.100.11.25

  3. [HTTP_X_REAL_IP] => 2.2.2.2, <a>

可以看到,HTTP_X_FORWARDED_FOR 和 HTTP_X_REAL_IP 是万万不可直接拿来用的。使用$remote_addr是明智的选择。

比如我们伪造一下来源IP发给著名的 ip138.com

<pre data-evernote-id="18" class="js-evernote-checked" style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 1em;">curl http://1212.ip138.com/ic.asp -H 'X-Forwarded-For: unkonw, <alert>aa,11.22.33.44,11</alert>" 1.1.1.1' </pre>

它原样输出了我们伪造的XFF。

2.2.在代理服务器或CDN之后的业务服务器

前提:上面的每一层代理或CDN,都将原始请求的 remote_addr 一路传递下去。我们先来看其中一种方案。

如果web服务器上层也是使用nginx做代理或负载均衡,则需要在代理层的nginx配置中明确XFF参数,累加传递上一个请求方的IP到header请求中。以下是代理层的nginx配置参数。

  1. proxy_set_header X-Real-IP $remote_addr;

  2. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

  3. proxy_set_header Host $http_host;

  4. proxy_set_header X-NginX-Proxy true;

如果web服务器前面使用了HAProxy,则需要增加以下配置来将用户的真实IP转发到web服务器。

<pre data-evernote-id="19" class="js-evernote-checked" style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 1em;"> option forwardfor </pre>

如果想在业务服务器获取完整的链路信息,还是通过XFF获取,需要在nginx的配置中加一条配置,加上此配置可以让我们获取整个链路信息:

<pre data-evernote-id="20" class="js-evernote-checked" style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 1em;">fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for; </pre>

实测此参数最好加在被 include 的fastcgi.conf中,就是有一堆fastcgi_param配置的那个文件否则就写入location段。这个配置可能会影响你的nginx日志,这个后续会详细说明。如果不配置此项,则我们在WEB SERVER 上直接获取到的XFF信息则是上一个代理层的IP。当然,也不影响获取用户真实IP。不过如果你是在调试配置的情况下,就不方便查看整个链路了。

2.2.1 在只有一层代理的情况下

我们按上面的配置发起一个伪造请求, 10.100.11.25 是我电脑的IP,链路为:

<pre data-evernote-id="21" class="js-evernote-checked" style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 1em;">10.100.11.25(client)->10.200.21.33(Proxy)->10.200.21.32(Web Server) </pre>

curl 请求:

<pre data-evernote-id="22" class="js-evernote-checked" style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 1em;">curl http://10.200.21.33:88/test.php -H 'X-Forwarded-For: unkonw, <8.8.8.8> 1.1.1.1' -H 'X-Real-IP: 2.2.2.2' </pre>

结果如下:

  1. [HTTP_X_FORWARDED_FOR] => unkonw, <8.8.8.8> 1.1.1.1, 10.100.11.25

  2. [REMOTE_ADDR] => 10.200.21.33

  3. [HTTP_X_REAL_IP] => 10.100.11.25

我们可以看到,XFF被附加上了我的IP,但前面的一系列伪造内容,可以轻易骗过很多规则,而HTTP_X_REAL_IP 则传递了我电脑的IP。因为在上面的配置中,X-Real-IP 已经被设置为握手 IP。 但多层代理之后,以上面的规则,显然 HTTP_X_REAL_IP 也不会是真实的用户IP了。而 HTTP_X_FORWARDED_FOR 则在原有信息(我们伪造的信息)之后附上了握手 IP 一起传递过来了。

2.2.2 在两层或更多代理的情况下

我们这里只测试两层,实际链路为:

<pre data-evernote-id="23" class="js-evernote-checked" style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 1em;">10.100.11.25(client)->10.200.21.34(Proxy)->10.200.21.33(Proxy)->10.200.21.32(Web Server) </pre>

Curl 命令:

<pre data-evernote-id="24" class="js-evernote-checked" style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 1em;">curl http://10.200.21.34:88/test.php -H 'X-Forwarded-For: unkonw, <8.8.8.8> 1.1.1.1' -H 'X-Real-IP: 2.2.2.2' </pre>

两层代理的情况下结果为:

  1. [HTTP_X_FORWARDED_FOR] => unkonw, <8.8.8.8> 1.1.1.1, 10.100.11.25, 10.200.21.34

  2. [REMOTE_ADDR] => 10.200.21.33

  3. [HTTP_X_REAL_IP] => 10.200.21.34

根据上面的情况,怎么挑出真正的用户IP呢?设想三种方案:

**1. ** 第一层代理将用户的真实 IP 放在 X-Real-IP 中传递下去,后面的每一层都使用 X-Real-IP 继续往下传递。配置为:

  1. proxy_set_header X-Real-IP $remote_addr; # 针对首层代理,拿到真实IP

  2. proxy_set_header X-Real-IP $http_x_real_ip; # 针对非首层代理,一直传下去

<pre data-evernote-id="26" class="js-evernote-checked" style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 1em;">**2.** 从首层开始,**将用户的真实IP 放在 X-Forwarded-For 中**,后面的每一层都使用 X-Forwarded-For继续往下传递。配置为:</pre>

<pre data-evernote-id="27" class="js-evernote-checked" style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 1em;"> 从首层开始,将用户的真实IP 放在 X-Forwarded-For 中,而不是累加各层服务器的 IP,但这样也不够合理,因为丢掉了整个链路信息。配置为:</pre>

<pre data-evernote-id="28" class="js-evernote-checked" style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 1em;"> proxy_set_header X-Forwarded-For $remote_addr; # 针对首层代理</pre>

<pre data-evernote-id="31" class="js-evernote-checked" style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 1em;"> 针对非首层代理,则可以用逐步累加的方法。配置为:</pre>

proxy_set_header X-Forwarded-For $http_x_forwarded_for; # 针对非首层代理

<pre data-evernote-id="33" class="js-evernote-checked" style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 1em;"> 从 X-Forwarded-For 中获取的用户真实IP,排除掉所有代理IP,取最后一个符合IP规则的,注意不是第一个,因为第一个可能是被伪造的(除非首层代理使用了握手会话 IP 做为值向下传递)。</pre>

一般CDN都会将用户的真实 IP 在XFF中传递下去。我们可以做几个简单的测试就能知道我们该怎么做。

注意:nginx配置的这两个变量:

3.配合nginx realip模块获取用户真实IP

我们应该秉承一个原则:

能通过配置让事情变的更简单和通用的事儿,就不要用程序去解决。即环境对程序透明。这当然少不了系统运维人员的辛苦。

如果能在配置中理清,就不必用复杂的程序去解决,因为Server上可能有各种应用都要来获取用户IP,如果规则不统一,结果会不一致。
程序不知道链路到底经过了几层才转到web server上,所以让程序去做兼容并不是个好主意。索性就让程序把所有的代理都当成透明的好了。

终于说到重点了。上面介绍的三种方法中,如果不能保证前面的代理层使用我们指定的规则,这时候怎么办呢?

只能使用第三种方法( 即:配合 nginx realip 模块获取用户真实IP)。

我们将各层代理的IP排除在外,就取到了真实的用户IP。这个可以使用nginx的一个模块 realip_module 来实现。

原理是从XFF中抛弃指定的代理层 IP,那么最后一个符合规则的就是用户 IP。也可以配合第一起方法一起使用。

但无论如何,首层代理的规则最重要,直接影响后面的代理层和web service的接收结果。

nginx realip_module 模块需要在编译nginx的时候加上参数--with-http_realip_module

然后在nginx配置中增加以下配置(可以在http,server或location段中增加)

  1. set user real ip to remote addr

  2. set_real_ip_from 10.200.21.0/24;

  3. set_real_ip_from 10.100.23.0/24;

  4. real_ip_header X-Forwarded-For;

  5. real_ip_recursive on;

set_real_ip_from 后面是可信 IP 规则,可以有多条。如果启用CDN,知道CDN的溯源IP,也要加进来,除排掉可信的,就是用户的真实IP,会写入 remote addr这个变量中。

比如在PHP中可以使用$_SERVER['REMOTE_ADDR'] 来获取。而WEB SERVER 不使用任何反向代理时,也是取这个值,这就达到了我们之前所说的原则。

real_ip_recursive 是递归的去除所配置中的可信IP。如果只有一层代理,也可以不写这个参数。

然后我在外网请求一下,结果是这样的

  1. [HTTP_X_FORWARDED_FOR] => unkonw, <8.8.8.8> 1.1.1.1, 112.193.23.51, 10.200.21.50

  2. [REMOTE_ADDR] => 112.193.23.51

112.193.23.51 是 client 的 IP, 10.200.21.50 是WEB SERVER 前面的负载均衡。 真实IP拿到了。

再说下nginx日志

如果nginx日志中记录了XFF,那么可能会有一些是我们不想记录的,比如我们现在使用的默认的nginx日志格式为:

  1. log_format main 'remote_addr -remote_user [time_local] "request" '

  2. 'statusbody_bytes_sent "$http_referer" '

  3. '"http_user_agent" "http_x_forwarded_for"';

这时候由于XFF里包含太多信息,甚至可能是一些伪造的未经过滤的文本,在使用和分析日志的时候会出现麻烦,所以我们干脆不记录它。nginx 的日志格式log_format还有一个默认值“combined”. 默认格式为:

  1. log_format combined 'remote_addr -remote_user [$time_local] '

  2. '"request"status $body_bytes_sent '

  3. '"http_referer" "http_user_agent"';

我们使用这个格式就好了。

总结

我们建议使用以下规则:

握手IP:即请求方的 remote_addr.

运维很重要,首个代理层的处理方式很重要。

在只有运维最清楚网络环境的时候,尽量通过配置对应用透明。减少应用层的复杂判断。如果环境很复杂,比如使用了CDN,则有可能需要多方协调。

上一篇下一篇

猜你喜欢

热点阅读