当我们给网站使用例如CDN,Nginx或Varnish等缓存服务时,为了获取访客的真实IP,大多数会地把访客的真实IP赋值给X-Forwarded-For(下文简称XFF)。

 

但是因为XFF是个HTTP请求头,也就是最前面带有http_,因此这类http信息就可以被伪造。

 

其实根据实际使用情况判断是否需要获取XFF内容就不会出现这些问题。

 

拿Nginx的反代理(Proxy模块)功能来说,有人会把$proxy_add_x_forwarded_for变量的内容传给后端作为用户的真实IP。

 

Nginx对该变量的处理非常智能,当有XFF传过来时,Nginx就会自动把Nginx服务器的IP加到原来的XFF最后面,再发给后端。

 

这智能也带来了问题,如果访客自己伪造了一个XFF变量内容,那样后端服务器所获取的访客IP也是假的,给不怀好意的人有机可乘……

 

对于最前端来说,访客IP的变量只有一个是真实且无法伪造的——$remote_addr,此变量最前面不带有http_。

 

对于使用了Nginx,Varnish之类的做前端,把$remote_addr(Varnish的变量名为client.ip)作为访客IP是最明智的选择。

 

Nginx:

 

proxy_set_header X-Forwarded-For $remote_addr;

proxy_set_header X-Forwarded-For $remote_addr;

 

Varnish:

 

set req.http.X-Forwarded-For = client.ip;

set req.http.X-Forwarded-For = client.ip;

 

另外,使用Varnish的朋友也需注意下默认的XFF处理方式:

 

if (req.restarts == 0) {

if (req.http.x-forwarded-for) {

set req.http.X-Forwarded-For =

req.http.X-Forwarded-For + “, ” + client.ip;

} else {

set req.http.X-Forwarded-For = client.ip;

}

}

if (req.restarts == 0) {        if (req.http.x-forwarded-for) {           set req.http.X-Forwarded-For =               req.http.X-Forwarded-For + “, ” + client.ip;        } else {            set req.http.X-Forwarded-For = client.ip;        }     }

 

可以看出,Varnish的默认对XFF处理方式和Nginx Proxy模块的$proxy_add_x_forwarded_for基本一样。也一样存在XFF欺骗的问题。

 

如果没有使用CDN,个人建议把判断XFF是否有内容的代码去掉,直接获取remote_addr传给后端:

 

if (req.restarts == 0) {

set req.http.X-Forwarded-For = client.ip;

}

if (req.restarts == 0) {            set req.http.X-Forwarded-For = client.ip;     }

 

当然,前面所提及的只是最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最最简单的情况。

 

假设有一个网站使用这样的环境:PHP解析引擎为Apache,Apache前面有一个Nginx做缓存,Nginx前面还有一个Varnish做缓存,Varnish前面又加一个内容分发网络(CDN,所有节点均使用Nginx),我如何实现在Apache处能获取到访客真实IP,且不会出现XFF欺骗的情况?

 

首先,要求后端服务器,均不能直接访问,只允许通过CDN服务器访问!否则可能出现欺骗XFF的情况。

 

最前端——CDN节点的Nginx以$remote_addr变量作为访客的真实IP!这是保证不被欺骗XFF的关键:

 

proxy_set_header X-Forwarded-For $remote_addr;

proxy_set_header X-Forwarded-For $remote_addr;

 

Varnish处,获取从CDN节点传来的XFF内容,赋值给XFF,传给后面的Nginx:

 

set req.http.X-Forwarded-For = req.http.X-Forwarded-For;

set req.http.X-Forwarded-For = req.http.X-Forwarded-For;

 

Nginx处,如果要使用limit_req模块,或记录日志等功能,需要realip模块,设置真实IP来源于Varnish服务器的IP,并告诉Real模块,哪个变量储存着访客的真实IP,然后把Varnish传过来带有真实IP的XFF赋值给XFF传给Apache:

 

server {

……

set_real_ip_from   varnish的IP,如果同一个服务器,就是127.0.0.1;

real_ip_header     存放真实IP的变量,一般是X-Forwarded-For;

……

 

location / {

……

proxy_set_header X-Forwarded-For $http_x_forwarded_for;

……

}

}

server {  ……  set_real_ip_from   varnish的IP,如果同一个服务器,就是127.0.0.1;  real_ip_header     存放真实IP的变量,一般是X-Forwarded-For;  ……   location / {    ……    proxy_set_header X-Forwarded-For $http_x_forwarded_for;    ……  }}

 

Apache处,需要安装rpaf模块,并告诉rpaf模块,安装varnish,nginx的服务器IP和储存访客真实IP的变量:

 

RPAFenable On

RPAFsethostname On

RPAFproxy_ips Varnish服务器的IP Nginx服务器的IP       #不同IP之间用空格隔开

RPAFheader X-Forwarded-for

RPAFenable OnRPAFsethostname OnRPAFproxy_ips Varnish服务器的IP Nginx服务器的IP       #不同IP之间用空格隔开RPAFheader X-Forwarded-for

 

可见,从最前端的CDN节点到最后端的Apache,变量XFF的一直没变,一直都是只有访客的真实IP,如果Nginx处只是缓存,甚至连realip模块都不需要,直接就把XFF传送给Apache,这极大的简化了后端的处理,不仅能获取到访客的真实IP地址,也不会出现伪造XFF的问题。

 

最后我顺便提一下,X-Forwarded-For只是个变量名,你完全可以改成其它你喜欢的名字,我全文都用了X-Forwarded-For,只是顺应大多数人长久以来的使用习惯……