背景
某业务的线上出现了大量爬虫请求,请求的是一个免登录的查询接口,由于该接口每次最多只能返回几十条数据,而爬虫想要获得全部的数据,因此爬虫程序启动了大量的查询请求来爬取数据。导致Nginx负载很高,影响其他接口的响应速度。
目标
- 首先,在不做扩容处理的情况下,优先降低Nginx负载,让其他接口响应速度恢复正常。
- 限制爬虫行为,不让爬虫对其他服务产生影响。
措施
- IP黑名单控制
- Nginx配置反爬虫的UA限制
- Nginx配置接口限速
措施一:IP黑名单控制
通过分析线上Nginx的access.log,找到了爬虫来源的IP地址,在第一时间通过配置黑名单,限制这些IP的请求。 但是实际上,为了爬取数据,对方拥有庞大的IP池,我们封掉一批,对方立刻会切换成另一批。很快我们就发现,通过IP黑名单拦截实在拦截不完。
措施二:Nginx配置反爬虫的UA限制
采取这个措施,也是根据上一步时分析access.log想到的。日志经过脱敏处理后信息如下:
js
[47.xxx.xxx.169] [-] [2024-04-09 00:25:08] [POST /api/xyz HTTP/1.1] 200 10291 [-] [python-httpx/0.22.0] [-] [93.30.253.146] 0.091 0.012(http://aaa.bbb.com/)]
其中'python-httpx/0.22.0'是这个爬虫的请求头。这个特征非常明显,于是我们决定加上UA限制封掉这一批爬虫。
当然,在动线上的配置之前,为了保险起见,还是先在测试环境做一下验证。
1. 模仿爬虫调用接口
准备一下测试脚本,用python-httpx的包发送请求
js
import httpx
import requests
url="https://aaa.bbb.com/api/xyz"
#方式一:用python.httpx调用接口
with httpx.Client() as client:
r = client.post(url)
print(r)
print(r.text)
#方式二:用python.requests调用接口
r2 = requests.post("url")
print(r2)
print(r2.text)
运行此程序,通过两种方式,都可以拿到200的响应码。由于没有传入必传参数,响应的text里拿到的是业务服务器返回的900001的错误码,这说明了请求通过了Nginx层,传入到了后端服务器。
js
<Response [200 ]>
{"code":"900001","msg":"miss game or user params","data":null}
<Response [200]>
{"code":"900001","msg":"miss game or user params","data":null}
2.修改NG的配置,添加UA限制
在NG配置的Server作用域范围内,先添加部分UA的限制:限制python-requests的UA,不限制python-httpx的情况下:
js
#forbidden UA
if ($http_user_agent ~ "python-requests|HttpClient|MJ12bot|Ezooms|^$" ) {
return 403;
}
修改完配置,重新加载NG的配置
js
nginx -s reload
再运行测试脚本,拿到结果如下:
js
<Response [200 ]>
{"code":"900001","msg":"miss game or user params","data":null}
<Response [403]>
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.19.10</center>
</body>
</html>
运行结果符合预期。 然后再添加python-httpx的限制,以及一些其他常用爬虫请求头的限制,配置如下:
js
#forbidden UA
#if ($http_user_agent ~ "node-fetch|axios|python-httpx|Bytespider|FeedDemon|JikeSpider|Indy Library|Alexa Toolbar|AskTbFXTV|AhrefsBot|CrawlDaddy|CoolpadWebkit|Java|Feedly|UniversalFeedParser|ApacheBench|Microsoft URL Control|Swiftbot|ZmEu|oBot|jaunty|Python-urllib|python-requests|lightDeckReports Bot|YYSpider|DigExt|YisouSpider|HttpClient|MJ12bot|heritrix|EasouSpider|Ezooms|^$" ) {
return 403;
}
待NG新配置生效后,再运行之前写好的测试脚本,此时两个请求都拿到了403的响应码:
js
<Response [403 Forbidden]>
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.19.10</center>
</body>
</html>
<Response [403]>
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.19.10</center>
</body>
</html>
验证通过后,我们将限制UA的NG配置在线上生效,NG的负载立刻就降下来了。
思考
懂技术的朋友们肯定知道,这个UA完全可以伪造,而且伪造起来非常简单。
js
headers = {
'User-Agent':'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5'
}
r3 = requests.post(url, headers=headers)
print(r3.request.headers)
print(r3)
print(r3.text)
比如我仍用刚才的python request的方式,增加一个headers,就可以轻松绕过措施二的限制。得到正常的响应:
js
{'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Length': '0'}
<Response [200]>
{"code":"900001","msg":"miss game or user params","data":null}
可见这个措施并不能完全防住爬虫的行为,但好在加上这个限制UA的配置后,对方没有及时做调整,给我们赢来了宝贵的时间采取其他保护措施。
措施三:Nginx配置接口限速
我们已经知道如果爬虫伪造了UA,还是能够绕过基础拦截。此爬虫启动后每秒产生的请求压力太大,仍会导致Ng负载过高,影响其他的服务请求。
我们的首要任务,是阻止爬虫给整体系统造成过高的负载压力。
在无法快速引入其他安全保护机制的情况下,仅在Ng层面,还有哪些操作空间呢?
此时可以考虑通过Ng本身的限速模块来达到目标。
Nginx的限流模块介绍
ngx_http_limit_req_module 是Ng的内置限流模块,可以通过在配置文件中开启相关配置,即可使用。 其配置方式如下:
- 限流区域的定义
用以定义限流的key(目前看应该key应该只能是IP),限流所占用的内存大小,限流速率等。
js
指令名称:limit_req_zone
指令语法:limit_req_zone key zone=name:size rate= number r/s
配置区域:http
配置示例:
js
http {
limit_req_zone $remote_addr zone=req_limit:100m rate=10r/s;
}
此配置的含义为:
定义一个名为req_limit的限速区域,该区域占用nginx的内存大小为100兆,以remote_addr为缓存区域的key,针对每个key限速为每秒10个请求。其中$remote_addr是nginx拿到的客户端IP。
- 限流规则配置
js
指令命令:limit_req
语法:limit_req zone=name [burst=number] [nodelay | delay=number];
配置区域:http、server、location
burst:瞬时请求的最大限制,如果超过了该number,就看后面是nodelay处理还是delay处理
nodelay:当瞬时请求量超过burst,或请求速率超过zone里的rate限制后,如果配置了nodelay,则超出的请求将直接返回503的状态码,不会做延时处理。
delay:当瞬时请求量超过burst,或请求速率超过zone里的rate限制后,在缓冲区的请求将被延迟处理,而不是直接返回503,直到请求降到限制以内后再返回正常响应。
配置示例:
js
server {
location /api/xxx {
limit_req zone=req_limit burst=15 nodelay;
# ...
}
}
此配置的含义:
限制"/api/xxx"接口的处理速率,启用名为req_limit的缓存区域,允许的瞬时最大请求为15,使用nodelay的响应策略。
- 限流配置和验证 按照上面的两个示例配置限流参数,对/api/xxx接口执行每秒10个请求的速率限制,允许的瞬时最大请求数是15。 (1)情况一
以5个并发请求,调用此接口时,得到的响应码均为200.
js
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
(2) 情况二
以10个并发请求,调用此接口时,得到的响应码均为200.
js
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
<Response [200]>
(3)情况三
以超过15个并发,比如20个并发请求,调用此接口时,出现了一小部分503的响应码
js
<Response [200]>
<Response [200]>
// 省略一些
<Response [200]>
<Response [503]>
<Response [503]>
<Response [200]>
<Response [200]>
<Response [503]>
<Response [200]>
可见限速配置已经生效。 当选取合适的参数配置到业务的生产环境Ng上后,实现了有效拦截大量的爬虫请求,维持了服务正常运行。
小结
这次实践的前提是突然出现了大量的爬虫,通过调用服务接口的方式,持续高并发地爬取业务数据。造成了系统负载量很高,影响到了其他的服务。
为了做应急保护,在没有引入其他保护机制的情况下,先后做了三种措施来阻止爬虫的行为:
- IP黑名单控制
- Nginx配置反爬虫的UA限制
- Nginx配置接口限速
经过这三条措施后,在很长的一段时间里,爬虫都没有再对业务系统造成破坏。这当然很有可能是因为该业务数据本身并没有极大的价值值得对方升级措施爬取。如果是这样的话,那上述简单防护措施已经可以达到目的。但对于有更高防护要求的系统,可以考虑接入更全面、保护措施更有力的安全系统,从而保护自身的系统免受爬虫干扰。