最近服务器接口一直被疑似爬虫爬取数据,之前从来都没有在意过这方面的东西。发现这个爬虫还是因为磁盘空间不足发现的。公司的接口都是通过 Nginx 反代的,所以 Nginx 一直都有日志记录的,这次爬虫爬取的速度非常的快,4 个小时有将近 300W 次的请求。从而导致日志异常庞大,最后触发磁盘使用量报警。磁盘容量报警后才发现有人恶意的请求服务器的接口,通过分析接口的参数发现攻击者一直在请求后端的一个接口,于是这几天都在和攻击者斗智斗勇。

大量请求导致数据库不可用

这次的爬虫或者说是攻击一直再使用手机号查询该手机号是否在系统中注册过,大量的请求导致进出系统的带宽明显不够用。公司的带宽一直都是很紧张的,并不是很富裕。这一次攻击直接导致系统反应速度大大的降低,不少接口都超时了。

应对策略:
临时提升服务器带宽,减少因为带宽导致的服务不可用


数据库压力增大

由于爬取的是用户是否注册过接口,每一次请求都会查询一次数据库,导致数据库无法承受高频率的访问。这里系统是没有缓存的(历史原因,公司项目小,使用的人也少,所以数据都没有啥缓存),但是这里加上缓存估计效果也不是很大,通过请求参数分析攻击者每一次都是使用一个新的手机号码去请求接口,这些手机号码在数据库中是没有的。也就是说缓存是 hit 不到的,查询数据库是肯定的了。

应对策略:
分析攻击者请求头,临时改变程序。
这次攻击者的请求头有几个很明显的特征,于是通过这些特征值在代码层面进行了拦截,防止这类恶意的请求访问数据库。这个措施也是临时的,毕竟如果攻击者发现爬取不到数据,估计会修改请求头或者跟换其他接口访问。


大量流量导致 Tomcat 链接数达到上限

做了以上的工作后发现系统任然还是很慢,虽然服务器带宽和数据库的压力通过监测都已不是瓶颈。这时估计是由于连接数太多,导致单个 Tomcat 达到瓶颈,这里我并不知道该用什么工具去监测 Tocmat 的连接数或者其他的参数,仅仅是猜想。

应对策略:
于是使用 Nginx 负载均衡 2 个 Tomcat 对外提供访问。使用多个 Tomcat 对外提供访问后,系统响应速度明显回到了正常的水平。


大量请求仍在继续

虽然通过以上的应对措施服务对外是可以正常访问了,但是异常的访问任然还在继续。Nginx 的日志任然在不停的滚动,身为一个有责任心的程序员是无法容忍这样的访问和攻击的。

应对策略:
通过 Nginx 日志上 $http_x_forwarded_for 上的 IP 地址,然后使用 Linux 的 iptables 进行 IP 封杀。
一开始是用手动的方式,也起一定的效果,但是好景不长,一会会爬虫就更换了 IP,试了几次,最后放弃这种方式。毕竟人哪能和机器抗衡。

于是开始纠结的一天,查阅了很多网上的资料。最终尝试使用 Nginx 的 ngx_http_limit_req_module 模块的 limit_req_zone 指令进行限流访问,防止用户恶意攻击刷爆服务器。该模块是集成在 Nginx 中的,不用额外安装。

1
2
3
4
5
6
7
8
limit_req_status 503; ## 可配置在 http, server, location 默认是 503
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; ## 可配置在 http 中

server {
location /search/ {
limit_req zone=one burst=5 nodelay; ## 可配置在 http, server, location
}
}

limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
第一个参数:$binary_remote_addr 表示通过remote_addr这个标识来做限制,“binary_”的目的是缩写内存占用量,是限制同一客户端 ip 地址
第二个参数:zone=one:10m 表示生成一个大小为10M,名字为one的内存区域,用来存储访问的频次信息
第三个参数:rate=1r/s 表示允许相同标识的客户端的访问频次,这里限制的是每秒 1 次,还可以有比如 30r/m 的,表示一分钟内限制 30 次访问

limit_req zone=one burst=5 nodelay;
第一个参数:zone=one 设置使用哪个配置区域来做限制,与上面 limit_req_zone 里的 name 对应
第二个参数:burst=5,重点说明一下这个配置,burst爆发的意思,这个配置的意思是设置一个大小为 5 的缓冲区当有大量请求(爆发)过来时,超过了访问频次限制的请求可以先放到这个缓冲区内
第三个参数:nodelay,如果设置,超过访问频次而且缓冲区也满了的时候就会直接返回 503,如果没有设置,则所有请求会等待排队

注意:我平时修改 Nginx 的配置时都是使用 kill -HUP 进程号 的方式热重启 Nginx 的,但是这里不适用,需要关闭 Nginx 后重启方可生效


其他用到的东西

nginx 日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
'$status $gzip_ratio - $body_bytes_sent - $request_time "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

log_format main escape=json '{ "@timestamp": "$time_iso8601", '
'"remote_addr": "$remote_addr",'
'"costime": "$request_time",'
'"realtime": "$upstream_response_time",'
'"status": $status,'
'"x_forwarded": "$http_x_forwarded_for",'
'"referer": "$http_referer",'
'"request": "$request",'
'"upstr_addr": "$upstream_addr",'
'"bytes":$body_bytes_sent,'
'"dm":$request_body,'
'"agent": "$http_user_agent" }';

都是些常用的参数,可以查阅其他资料,学习具体参数是什么意思。

1
2
3
4
set $Real $http_x_forwarded_for;
if ( $Real ~ (\d+)\.(\d+)\.(\d+)\.(\d+),(.*) ){
set $Real $1.$2.$3.$4;
}

这个是由于原来的 Nginx 配置使用 $remote_addr 没有办法拿到用户的 IP 地址。而 $http_x_forwarded_for 可以,于是取 $http_x_forwarded_for 的 IP。上面的代码做了一个简单的拆分,取 $http_x_forwarded_for 的第一组 IP。
我这了设置了 $Real 作为限流的 key,所以 limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; 中的 $binary_remote_addr 就可以 跟换成 $Real 作为 key。这里还可以优化一下,把它转换成二进制的会更节省空间。