本篇博客将 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 6648RFC 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 这个草案付诸实践吧。

NGINX杂谈——flask_limiter的IP获取(怎么拿到真实的客户端IP)的更多相关文章

  1. Nginx+Docker部署模式下 asp.net core 获取真实的客户端ip

    目录 Nginx+Docker部署模式下 asp.net core 获取真实的客户端ip 场景 过程还原 结论 参考资料 Nginx+Docker部署模式下 asp.net core 获取真实的客户端 ...

  2. Django 如何获取真实远程客户端IP

    问题简述 我们知道HttpRequest.META字典包含所有HTTP头部信息(可用的头部信息取决于客户端和服务器).一般情况下,HttpRequest.META.get('REMOTE_ADDR') ...

  3. 获取SQL Server中连接的客户端IP地址[转]

    有时候需要获取连接到SQL Server服务器上的客户端IP地址,用什么办法呢? SELECT *FROM sys.dm_exec_connections WHERE session_id = @@S ...

  4. 配置nginx以获取真实的客户端ip地址

    当我们使用了nginx来转发客户端的请求以后,tomcat是无法正确获取到客户端的ip地址的,而是获取到配置了nginx的那台服务器的ip地址.因为tomcat所接收到的请求是通过nginx发出来的( ...

  5. NGINX前端代理TOMCAT取真实客户端IP

    nginx前端代理tomcat取真实客户端IP 使用Nginx作为反向代理时,Tomcat的日志记录的客户端IP就不在是真实的客户端IP,而是Nginx代理的IP.要解决这个问题可以在Nginx配置一 ...

  6. nginx+tomcat集群配置(3)---获取真实客户端IP

    前言: 在初步构建的nginx+tomcat服务集群时, 发现webserver获取到的客户端ip都是同一个, 皆为作为反向代理服务的nginx所在的机器IP. 这不太符合我们的基本需求, 为将来的数 ...

  7. nginx反向代理node.js获取客户端IP

    使用Nginx做node.js程序的反向代理,会有这么一个问题:在程序中获取的客户端IP永远是127.0.0.1 如果想要拿到真实的客户端IP改怎么办呢? 一.首先配置Nginx的反向代理 proxy ...

  8. nginx 获取源IP 获取经过N层Nginx转发的访问来源真实IP

    1. nginx 配置文件中获取源IP的配置项 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; #一般的we ...

  9. nginx中获取真实的客户端访问IP

    date : 2019-06-28 16:54:50 author: headsen chen notice: 个人原创 1,必需要先搞清楚的基本概念 1.1   什么是remote_addr     ...

随机推荐

  1. kernel_thread()和kthread_run()/kthread_create()的根本区别

    0 本质区别 kthread_run()调用kthread_create(), kthread_create()加入链表后,有kthreadd()线程读取链表然后再调用kernel_thread()创 ...

  2. Vue CSS模拟菜单点击变色

    <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8&quo ...

  3. weblogic漏洞分析之CVE-2021-2394

    weblogic漏洞分析之CVE-2021-2394 简介 Oracle官方发布了2021年7月份安全更新通告,通告中披露了WebLogic组件存在高危漏洞,攻击者可以在未授权的情况下通过IIOP.T ...

  4. 【Python学习】print语句

    一.print 可以向屏幕上输出信息,print 后面一个空格再加上''中间放入要输出的内容. 二.print可以用逗号分隔语句,但是每有一个逗号就会出来一个空格. 1 >>> pr ...

  5. PHP多文件上传格式化

    文件上传是所有web应用中最常见的功能,而PHP实现这一功能也非常的简单,只需要前端设置表单的 enctype 值为 multipart/form-data 之后,我们就可以通过 $_FILES 获得 ...

  6. TP5.0版本mysql查询语句 闭包

    Db::name('tiwen') ->where('user_id', $user_id) ->where(function ($query) { $query->where(fu ...

  7. Shell系列(12)- 预定义变量(5)

    预定义变量 作用 $? 常用:最后一次执行的命令的返回状态. 如果这个变量的值为0,证明上一个命令正确执行:如果这个变量的值为非0(具体是哪个数,由命令自己来决定),则证明上一个命令执行不正确了 $$ ...

  8. filter_var() 验证邮箱、ip、url的格式 php

    验证邮箱格式的正确与否:你的第一解决方案是什么呢? 不管你们怎么思考的:反正我首先想到的就是字符串查找看是否有@符号: 但是对于结尾的.com或者.net 亦或者.cn等等越来越多的域名验证感觉棘手: ...

  9. HTML 网页开发、CSS 基础语法——七.HTML常用标签

    标题标签(h1-h6) 1.标题标签 ① 标题(Heading),通过<h1>-<h6>六个标签分别来对六个级别的标题进行性定义的. ② <h1>是级别最高,也是字 ...

  10. P4100-[HEOI2013]钙铁锌硒维生素【矩阵求逆,最大匹配】

    正题 题目链接:https://www.luogu.com.cn/problem/P4100 题目大意 给出\(n\)个线性无关的向量\(A_i\),然后给出\(n\)个向量\(B_i\),求一个字典 ...