记 Nginx auth_request 指令中的小坑

小柊 发表于 2020年10月28日 1时58分38秒

序、背景

最近某个项目需要做系统平台的访问控制功能。具体说就是需要可以限制只允许指定的IP段访问系统Web页面,非指定的IP/IP段访问只能看到403的错误页。

这个需求在某种情况下倒也挺简单,Tornado的话,在RequestHandler的prepare阶段,检查一下客户端IP地址是否在IP段内,不在的话抛一个HTTPError(403);ASP.net core的话,注册一个AuthorizationFilter做IP检查即可。

问题在于笔者负责的项目是前后端分离的,后端这边只负责接受HTTP API的请求。前端资源直接交由Nginx负责响应。故即便在Web项目里做了访问IP检查也只能对API请求做访问限制。静态资源还是可以被自由访问。

如果需要对Nginx负责的前端资源也设置访问控制,就需要单独做一些设置了。

 

一、Nginx黑白名单

最容易想到也是最简单的方案就是使用Nginx自带的黑白名单功能,Nginx的黑白名单功能由ngx_http_access_module模块提供。

我们在站点配置文件中加入一条include指令,使得从外部导入一个配置文件,在这个配置文件里我们填入如下格式的内容:

 

这样就设置了当前站点只允许10.20.129.100这个IP地址和192.168.1.0/24这个IP段访问,除此以外其他访问的IP全部被拒绝。

 

但这种方案有两个问题:

1、笔者产品经理要求用户在配置访问白名单时,还允许用户输入如192.168.1.100-192.168.1.200这类格式的IP地址段;

2、每次修改完白名单列表配置文件后,需要调用如nginx -s reload命令来重启Nginx使得配置生效,并不灵活。

 

二、(倒也不是不能做…)

上面一章中提到了Nginx黑白名单功能只允许配置单个IP或CIDR格式的IP地址段,而笔者产品经理要求的“起始IP-结束IP”格式的IP段并不被Nginx所支持。

但!

也不是不能做。

 

我们把这种奇葩格式的IP地址段转成CIDR格式不就完了么。

很幸运的,Python自带的ipaddress模块就提供了这种功能(Python 2及3.2版本需要手动安装ipaddress软件包)。

 

调用ipaddress模块的summarize_address_range函数,传入起始IP和结束IP,即可得到指定IP段等效的CIDR格式列表。

然后我们再把这些CIDR地址往配置文件里写就完事了。

 

三、auth_request模块

由于使用Nginx黑白名单功能来实现访问控制功能的话,会碰到以下几个问题:

1、变更配置不灵活,每次修改IP配置后,需要重启Nginx来更新配置;

2、在使用Tornado fork_processes启用多进程的环境下,可能会出现多个用户同时修改配置造成多进程并发的竞态问题。

 

笔者后面决定使用Nginx的auth_request指令来实现访问控制的功能,Nginx的auth_request指令功能由ngx_http_auth_request_module模块提供。

简单介绍一下auth_request指令吧。

auth_request指令允许放在http、server和location上下文中,在配置好后,每当指定的作用域在收到HTTP请求时,Nginx会向指定的路径发起一个GET类型的子请求,子请求的请求头部分与原HTTP请求的请求头部分一致。

如果子请求收到2xx响应代码,则Nginx将允许原HTTP请求,如果子请求返回401或403响应代码时,则Nginx将使用相应的错误代码拒绝原HTTP请求,其他响应代码,则被视为错误。

 

相比于原先使用Nginx黑白名单的方案,新方案有一个非常突出的优点就是非常灵活:

后端只需要提供一个新API接口,负责处理Nginx的鉴权子请求,用户在配置界面上做的任何变更在保存后可以立即生效,且判断IP是否允许访问的逻辑由后端全权负责,什么样的IP/IP段格式后端只要实现了都可以支持。

当然也有代价:性能。

不过性能问题倒也不大,因为本身笔者负责的系统后台访问量就不高,能访问的IP地址就那么几个。在指定IP完成鉴权判断后,可以直接丢到Cache里缓存,直到下次用户修改对应配置时再淘汰掉即可。

 

接下来的工作就很简单了:

1、实现一个IP访问鉴权接口

笔者这边假设接口地址为/api/v1.0/auth/access,此接口会从请求头中的X-Real-IP头中获取客户端真实的IP地址。

2、修改Nginx的配置

原先的Nginx配置文件内容是这样的

 

咱们现在修改配置如下:

 

OK。然后笔者自测了一下,没发现啥问题就更新开发环境了。

然后……然后就踩坑里了。

 

四、噗通踩坑

更新完开发环境之后,笔者就去忙别的事情了。

没过一会儿,测试就匆匆跑过来告诉我:“系统登不上去了!”

由于笔者在更新环境前系统就是已登录的状态,笔者赶紧登出系统,在登陆界面输入用户名和密码后,一点登陆按钮,系统就一直在那儿转圈圈。

 

调出开发者工具,查看网络选项卡,可以看到登陆请求一直处在等待状态:

难不成是Web后台卡了?但诸如获取验证码这样的接口却一切正常,可以正常响应而且基本没有响应延迟,可见后台Web还是在正常工作的。

翻了一下后台Tornado进程的访问日志和Nginx的访问日志,发现Nginx则在浏览器中的登陆请求超时后,出现了一条响应代码499的访问日志。

按照Nginx对499错误代码的定义,应该是Tornado服务端没有及时的响应请求,造成客户端超时断开连接造成的。

但神奇的是Tornado的访问日志中,并没有发现对应接口的请求日志。

Tornado没有响应日志,一般有以下几个原因:

1、Tornado没有收到对应的请求,故自然没有请求日志;

2、Tornado至今没有处理完请求(Tornado是在完成响应后才会输出访问日志,因为Tornado需要记录接口响应时间)。

 

那怎么确定是哪种情况呢?

遇事不决,先抓个包看看。在服务器上安装tcpdump,并使其捕获本地环回接口上端口为8080的包(笔者这边Tornado监听的就是本地环回的8080端口),然后重新在页面上尝试登陆,最后在tcpdump捕获的结果里寻找是否存在Nginx对Tornado进程发起了鉴权鉴权子请求。

稍稍费了一点小功夫,从一堆数据包中找到了鉴权子请求(由于没有响应包,Wireshark没有自动识别出HTTP请求),捕获数据包如下:

可以看到,Nginx确实向Tornado服务发起了鉴权子请求,但Tornado迟迟没有进行响应,最终Nginx“忍无可忍”,超时关闭了连接。

不过重点来了:既然我们的前端资源能够正常的加载,就说明这个/api/v1.0/auth/access接口在某种情况下还是可以正常工作的。

那为什么有的鉴权子请求可以正常响应,而有些鉴权子请求却又不能响应了呢?

 

五、真相大白

其实真相远比想象的简单。其实很多细心的人在看刚刚抓包的截图的时候就发现问题了。

不能正确响应的鉴权子请求里出现了Content-Length请求头。

一个没有请求正文的GET请求里出现了Content-Length请求头……

还记得笔者在第三章里介绍auth_request指令时说的内容吗?

Nginx会向指定的路径发起一个GET类型的子请求,子请求的请求头部分与原HTTP请求的请求头部分一致

Nginx它把Content-Length请求头也带过去了= =。

而Tornado在收到带有Content-Length的请求时,会继续等待正文部分的数据接收完毕后,才会进入后续的业务代码。

(下图摘自Tornado 6.0.3源码:tornado/http1connection/py:589

也就是说,这问题的大致情况就是:

Nginx:大兄弟,处理一下这个请求

GET /api/v1.0/auth/access HTTP/1.1

...balabalabala...

Content-Length: xxx

...balabalabala...

 

Tornado:好。

 

(过了很久)

(Tornado:这傻x怎么还没把剩下的xxx字节数据发过来?)

(Nginx:这傻x怎么还没响应我?)

 

同理,只要源请求中带有Content-Length请求头,鉴权接口都会出现无法返回的问题。

找了其他同事已经登陆后台的环境测了一下,果然什么POST的接口统统不能访问。

 

六、解决方案

解决方案倒也简单。

虽说主要是因为Tornado在检查到请求头中存在Content-Length头后不管请求的类型,铁了心的要收指定长度的body才能进入请求处理阶段。但咱们总不可能因为这个原因去改Tornado的源码吧。

说到底Tornado也没错,谁让Nginx发送子请求的时候明明没有Body还设置Content-Length头啊,所以我们还是改Nginx的配置文件,让Nginx在发送鉴权子请求时,不再带上Content-Length头。

在Nginx的配置文件里为鉴权接口单独写一个location,并手动指定Cotent-Length请求头为空即可。

修改后的配置文件为:

 

重启Nginx以加载最新配置,问题解决。

 

七、写在最后

本来并不想写这篇文章的,因为这个坑其实并不是什么大坑(就是很生草)。

但主要是一开始排查问题的时候,没有看到nginx的超时请求日志,完全是一副无头苍蝇的样子在那儿乱排查,Google了好久也没找到类似的案例。希望这篇博客能为后面来的朋友提供帮助。

当然,也要批评一下自己,自测不彻底就直接更新环境,活该被测试吊着锤。

 

 

 

小柊

2020年10月28日 01:39:16

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注