问题本身
Nginx是一个功能强大的Web服务器,它的强大体现在它的灵活性上。为了提高访客的访问速度,Nginx可以前置各种各样的CDN;为了实现复杂的功能,Nginx也可以前置TCP四层代理。然而,这些后置Nginx的做法有一个需要亟待解决的问题:如何让Nginx获取到访客的真实IP。
如果只是前者,一个CDN前置于Nginx,解决这个问题非常简单:只需要为Nginx安装ngx_http_realip_module模块,简单配置一下就可以。例如,Cloudflare会通过向后端的Nginx发送X-Forwarded-For请求头,其中就包含了访客IP。因此,只需在Nginx配置文件中加上一句real_ip_header X-Forwarded-For;就完美解决了。
如果只是后者,Nginx后置于TCP四层代理,则可以试试通过Proxy Protocol传递访客IP,当然这需要TCP四层代理软件的支持。同样需要ngx_http_realip_module模块,也是在Nginx配置文件中添加简单一句real_ip_header proxy_protocol;就可以。
然而,让我们考虑以下两个极端情况:
- 前面所说的两种情况同时出现,即既需要从CDN中获取访客IP,又需要在四层代理中获取。
- 同时使用两家CDN,两家CDN向后端Nginx发送的请求头不同,如CDN A发送
X-Forwarded-For,CDN B发送X-Client-Real-Ip。下图描述的就是这种情况。
real_ip_header指令只能使用一次,配置文件中重复出现的指令将会报错:
因此在这两种情况下,要想获取访客的IP地址,只能二选一,要么选择A放弃B,要么选择B放弃A。真的没有办法了吗?
可能的解决方法...?
其实思路还是很简单的,最简单的方法还是对请求进行判断,如果请求来源于A,就使用A的请求头中的访客IP;如果来源于B,就使用B的请求头中的访客IP。在Nginx中,这一点可以通过map指令来实现,下面就是一个判断访客IP是否来自于Cloudflare CDN的demo:
map $HTTP_CF_CONNECTING_IP $clientRealIp {
"" $remote_addr;
~^(?P<firstAddr>[a-z0-9.:]+),?.*$ $firstAddr;
}
这条map指令创建了一个名为 $clientRealIp 的变量,通过 $HTTP_CF_CONNECTING_IP 变量的值进行映射。
"" $remote_addr;:如果$HTTP_CF_CONNECTING_IP变量为空(也就是访客来源并非是Cloudflare),则使用$remote_addr作为$clientRealIp。$remote_addr是请求直接到达Nginx服务器的客户端IP地址。当然,也可以换成其他CDN获取到的访客IP地址。~^(?P<firstAddr>[a-z0-9.:]+),?.\*$ $firstAddr;:如果$HTTP_CF_CONNECTING_IP变量有值,则使用正则表达式从中提取第一个IP地址。$HTTP_CF_CONNECTING_IP中包含了Cloudflare传递的客户端真实IP地址。
现在,$clientRealIp中的值就是访客的真实IP了,如果将它放到real_ip_header指令中,似乎问题就能...解决啦?
然而并不可以,你会发现日志中获取到的还是CDN的回源IP,并没有获取到访客的IP。这是因为real_ip_header指令中后面跟的参数是一个Header而不是一个String,我们获取到的$clientRealIp是一个访客IP的字符串,real_ip_header指令并不认识它。
要想让它认识,需要将访客IP字符串包装成一个Header,这就需要用到另外一个模块,nginx-module-headers-more。下面就利用这个模块正确的从多个来源获取访客的IP。
问题的解决
Step1. 重新编译Nginx
第一步需要重新编译Nginx,将nginx-module-headers-more作为参数编译到Nginx中;当然,如果你的Nginx编译参数中原本就没有ngx_http_realip_module模块,编译的时候不要忘了一并添加进来。
nginx-module-headers-more模块的相关文件需要在这里下载:https://github.com/openresty/headers-more-nginx-module
wget https://github.com/openresty/headers-more-nginx-module/archive/refs/tags/v0.37.zip
unzip v0.37.zip
mv headers-more-nginx-module-0.37 headers-more-nginx-module
cd nginx-1.25.5
./configure [other-args] --with-http_realip_module --add-module=../headers-more-nginx-module
make
make install
以上命令仅供参考,这样就完成了编译安装。当然,你也可以采用使用动态链接的方法引入模块,作用相同,在此不做赘述。
Step2. 配置Nginx
这里用从Cloudflare CDN和TCP四层代理两处来源获得访客IP做例子来进行演示,其他情况可以参照着这个来。
#nginx.conf
...
map $HTTP_CF_CONNECTING_IP $clientRealIp {
"" $proxy_protocol_addr; //Proxy Protocol获取到的四层TCP代理访客IP
~^(?P<firstAddr>[a-z0-9.:]+),?.*$ $firstAddr;
}
...
#vhost
...
more_set_input_headers "X-IP: $clientRealIp";
real_ip_header X-IP;
...
more_set_input_headers:利用nginx-module-headers-more模块,自定义一个名为X-IP的请求头,请求头的内容就是我们获取到的字符串格式的访客IP。real_ip_header:使我们自定义的X-IP请求头生效。
实际上操作起来非常简单,原理同样也通俗易懂。重启Nginx,你的Nginx应该能够从两个不同的来源获取访客IP了。当然,如果要从多个来源获取的话,原理也相近,只是map指令要复杂一些。
之前本站采取了偷懒的做法,直接使用了参考资料链接一中的方法,将访客IP直接写入日志,因为那时候本站只需要将访客IP写入日志进行分析。但是要在Nginx中进行一些复杂的操作就会立即露马脚,比如根据访客的IP进行封禁,Nginx本身拿到的还是CDN的回源IP,直接写规则会是失效;而本站最近在Nginx侧启用了WAF,问题就更加严重了。经过上面的摸索解决了我的问题,希望也能帮到你。
PS. 如果正经做站或者有业务,强烈建议屏蔽腾讯云、阿里云和Ucloud的IP,没有真人流量,全是各种爬虫和攻击流量。本站已经全数对它们进行屏蔽。
参考资料
使用CF CDN服务后,nginx日志文件记录中的真实ip问题 | 未末
How to use multiple real IP headers with nginx - GetPageSpeed












看来屏蔽阿里云是对的 https://www.v2ex.com/t/1178158
:no-location:
是这样的hhhh
阿里 腾讯 Ucloud DigitalOcean Choopa这类相对低门槛的云服务都最好屏蔽,或者上CF的质询。
歪个楼,哪天我用腾讯云服务器代理访问合法的境外网站,例如贵站不过都用 cf cdn,不如只允许 cf ip 访问
:no-location:
这里的代理不是说绕过审查,只是避免暴露家宽 ip
:no-location:
可以的,服务器端记录的是你的腾讯云服务器的IP,可以避免你家宽IP的暴露。
你好像没有理解我的意思,此时我用腾讯云 ip 访问贵站不就被屏蔽了
:no-location:
当然是可以的,如果只用CF的IP的话,只允许CF访问源服务器,是防止源站IP暴露的一种很好的方法
CF会把访客的IP传递给源站的
请问一下,我用的是内网穿透FRP(支持proxy protocol),web服务端用的是openresty(1Panel用的),但是我不知道如何让openresty能够处理frpc传过来的proxy protocol请求头,请问也适用于这篇文章吗?
抱歉,你的评论被系统吞了,这才看到(
我觉得你的需求直接在openresty配置文件的listen这一行,添加proxy_protocol就可以了吧,这样就直接可以从frp添加的proxy protocol请求头里面拿到客户端的IP了。
也可能是我没明白你的意思,你的问题似乎没有涉及到处理多个来源访客IP的问题,frp的文档应该对你有帮助:https://gofrp.org/zh-cn/docs/features/common/realip/
之前我尝试是用Lua去解决融合CDN传递客户端真实IP不同的情况。看了博主的文章又学到了新的方法~ :no-location: