NGINX杂谈——flask_limiter的IP获取(怎么拿到真实的客户端IP)
本篇博客将 flask_limiter 作为切入点,来记录一下自己对 remote_addr 和 proxy_add_x_forwarded_for 两个变量、X-Real-IP 和 X-Forwarded-For 两个字段的一些理解。
flask_limiter 的文档。
如果开发过 Flask + NGINX 的项目,又使用了 flask_limiter 做 IP 限制,就有可能会遇上所有用户共享限制的问题(一个用户用超次数,另一个用户也无法使用)。这是因为使用了 flask_limiter 提供的默认 key_func——get_remote_address 经过 NGINX 代理拿不到真实的用户 IP。接下来,我们通过他的源码来分析具体的原因,下文可能很长,且没有直接了当的解决方法,如果只为解决问题,建议搜索其他文章。
flask_limiter 的 IP 获取
以下是 flask_limiter 获取 IP 的代码
def get_remote_address():
"""
:return: the ip address for the current request (or 127.0.0.1 if none found)
"""
return request.remote_addr or '127.0.0.1'
可以看到 flask_limiter 是通过 request.remote_addr 获取的IP。这里的 remote_addr 变量和 NGINX 的 $remote_addr 是一样的,都是直接从 TCP 连接信息中获取的,基本上不能被伪造。即使伪造了,TCP 连接都不知道你是谁,无法进行三次握手,那就根本建立不了连接。
所以 remote_addr 可以理解为就是和当前服务正在通信的客户端的真实 IP 地址。
而如果加入 NGINX 代理,再进行 http 访问的时候,用户就不再和 Python 服务直接建立链接。正在通信的客户端就不再是真实的客户端,而是代理客户端。
我们通过netstat -n | grep -E '\.2081|\.80'
来查看 TCP 连接情况也可以证实这一点。根据输出可以发现用户(192.168.17.167)和 NGNIX 服务端(192.168.19.165:80)建立了 TCP 链接,NGINX 客户端(127.0.0.1:64671)和 Python 服务端(127.0.0.1:9001)建立了 TCP 链接。所以经过 NGINX 代理,这个时候 Python 服务端拿到的 remote_addr 就是 NGINX 客户端的 IP。
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 192.168.19.165.80 192.168.17.167.47362 ESTABLISHED
tcp4 0 0 192.168.19.165.80 192.168.17.167.47360 ESTABLISHED
tcp4 0 0 192.168.19.165.80 192.168.17.167.47130 TIME_WAIT
tcp4 0 0 127.0.0.1.9001 127.0.0.1.64671 TIME_WAIT
既然我们无法通过 request.remote_addr 来获取真实的用户 IP,那我们就只能在 NGINX 代理的时候设置请求头,然后在 Python 服务端通过请求头来获取用户的真实 IP 了。flask_limiter 的问题就可以通过自定义 key_func 去获取我们在 NGINX 设置的请求头来解决。
NGINX 常见的和 IP 有关的设置有两个:
location /flask/ {
proxy_pass http://localhost:9001/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
以上是两种常见的设置方法,但是我们还是要理清楚他们的原理。这样我们进行多层 NGINX 代理,或者用别的代理时才能避免错误。
X-Forwarded-For
HTTP/1.1(RFC 2616)协议并没有对 X-Forwarded-For 进行定义,所以 X-Forwarded-For 一直以来都不是标准的HTTP头信息,IANA 的注册信息也可以佐证这一点。我看网上其他的一些博客说它后来被写入 Forwarded HTTP Extension(RFC 7239)标准,甚至连百度百科也说,IETF 在 Forwarded-For HTTP 头字段标准化草案中正式提出。我印象中 RFC 7239 就是因为 X-Forwarded-For 不标准才提出 Forwarded 这个新的请求头字段。为此,我去翻看了 RFC 7239 的历史版本,确认了没有哪一版 RFC 7239 为 X-Forwarded-For 正名过。如果看过 RFC 6648 或 RFC 7231 的 "8.3.1. Considerations for New Header Fields"小节,就会知道"X-"开头的头信息字段其实是不被认可的。
作为一个不标准的请求头字段,X-Forwarded-For 却被各大 http 代理、负载均衡等转发服务追捧,尽管 RFC 7239 提案已经进入 proposed standard 状态,也无法改变 X-Forwarded-For 的地位。
X-Forwarded-For的工作原理很简单,只要每一个代理服务都在定义 X-Forwarded-For 时都追加上上一个代理或客户的 IP(这是真实的,无法被仿造的),就可以记录下 http 请求链中的所有IP地址,以便后续的每个服务访问。
有一些爬虫教程会介绍通过修改 X-Forwarded-For 绕过 IP 限制的方法,但这种方法其实是在利用网页服务开发者的不严谨,并不是真的欺骗了网页服务。正如上文所说,一个合格的代理,是会追加上上一个代理或客户的IP的。比如 NGINX 的 $proxy_add_x_forwarded_for,就是会包含真实的 IP 的,他的值为客户端请求头的 X-Forwarded-For字段的值 + 客户端的真实 IP。
Eg. 如果客户端(0.0.0.0)请求头的 X-Forwarded-For 字段为
127.0.0.1
,则 proxy_add_x_forwarded_for 为127.0.0.1, 0.0.0.0
;如果客户端(0.0.0.0)请求头的 X-Forwarded-For字段为127.0.0.1, 196.128.0.1
,则 proxy_add_x_forwarded_for 为127.0.0.1, 196.128.0.1, 0.0.0.0
。所以设置得当的话,X-Forwarded-For 和 X-Real-IP 都是可以拿到真实的 IP 的。
有一些防伪造 IP 的教程会说要把 X-Forwarded-For 和 X-Real-IP 一样设置为 $remote_addr,这其实也是不合理的。X-Forwarded-For 和 X-Real-IP 诞生的目的是不一样的,X-Real-IP 是为了记录最外一层代理面向的 IP,保障对服务和代理(这里说的代理指的是由网页服务提供方提供的代理,可以理解为反向代理)这个整体来说,请求来自合法的 IP;X-Forwarded-For 则是为了记录完整的 IP 链,保障对客户来说,不管他使用什么样的代理(这里说的代理指的是由网页服务提供方提供的代理加上用户自己使用的代理,比如 VPN),只要代理可靠且不以隐匿为目的,网页服务总能契合他的需求。
Eg. 就像天气一样,我们不能因为用户使用了北京的代理,就给他推北京的天气,我们要追踪到最原始的 IP 地址,并且给他推送该IP对应的地区的天气。
X-Real-IP
如果说 X-Forwarded-For 是一个野孩子,那 X-Real-IP 则更是浮萍漂泊。好歹 NGINX 代理给 X-Forwarded-For 专门设了一个传递变量,X-Real-IP 却只能是 remote_addr 的载体,甚至随便换个名字也没有影响,我们只需要起一个 NGINX 支持的,又没有在标准里的名字就可以,叫阿猫阿狗也行。
location /flask/ {
proxy_pass http://localhost:9001/;
proxy_set_header A-MAO-A-GOU $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
这样设置最后通过 A-MAO-A-GOU 也可以拿到真实的 IP。当然,采取和大家一样的标准是很重要的。
上文介绍了 X-Forwarded-For 会存整个 IP 链,那我们通过 IP 链就可以反推会最外层代理(这里说的代理指的是由网页服务提供方提供的代理,可以理解为反向代理)面对的客户端了。
比如,X-Forwarded-For 字段的值是0.0.0.0, 1.1.1.1, 2.2.2.2, 3.3.3.3, 4.4.4.4
,我们知道3.3.3.3和4.4.4.4是我们的代理服务器,那最外层代理面对的客户端就是2.2.2.2。
不过一般来讲,这样操作需要开发人员去了解代理的情况,这对开发人员也是一个负担,如果代理不只是一条链路就更麻烦。所以更好、更直接的方法就是商量好一个字段,让大家都使用它来传递信息,比如 X-Real-IP。这样代理服务通过X-Real-IP给出一个IP,开发人员利用这个IP进行简单的校验;而代理服务通过 X-Forwarded-For 给出的IP链,开发人员往往取最前的一条,当成客户的真实IP。
以上就是关于 X-Real-IP 和 X-Forwarded-For 的记录,希望有生之年可以看到 RFC 7239 这个草案付诸实践吧。