Nginx中使用 Lua+Redis 限制IP的访问频率

阅读 1627
收藏 87
2017-12-29
原文链接:www.zifangsky.cn

前言:现在某个生产系统遭到爬虫频繁抓取数据,已经严重影响到正常用户使用,因此需要想办法降低这些恶意请求。我最开始想的是利用Nginx自带的参数来限制,但是经过测试发现并不好使。因此最终决定使用Lua+Redis来限制IP的访问频率,于是也就有了这篇文章

一 技术准备

(1)Nginx中的Lua编程:

关于这点我选择了安装OpenResty。OpenResty,也被称为“ngx_openresty”,是一个以Nginx为核心同时包含很多第三方模块的Web应用服务器。关于Linux下的OpenResty安装和使用可以参考我之前的这篇文章:www.zifangsky.cn/1020.html

另外,关于在Nginx中使用Lua编程的基本用法可以参考我之前的这篇文章:www.zifangsky.cn/1024.html

(2)单位时间内单个IP的访问次数存储:

由于保存的数据量不大而且保存的时间很短(30秒自动过期),因此我选择了现在最常使用的Redis。关于Redis单节点的安装可以参考我之前的这篇文章:www.zifangsky.cn/823.html

二 代码实现与测试

(1)添加访问控制的Lua脚本:

[root@hbase31 ~]# vim /usr/local/openresty/nginx/conf/lua/access.lua

其内容如下:

local ip_block_time=300 --封禁IP时间(秒)
local ip_time_out=30    --指定ip访问频率时间段(秒)
local ip_max_count=20 --指定ip访问频率计数最大值(秒)
local BUSINESS = ngx.var.business --nginx的location中定义的业务标识符
 
--连接redis
local redis = require "resty.redis"  
local conn = redis:new()  
ok, err = conn:connect("192.168.1.30", 6379)  
conn:set_timeout(2000) --超时时间2秒
 
--如果连接失败,跳转到脚本结尾
if not ok then
    goto FLAG
end
 
--查询ip是否被禁止访问,如果存在则返回403错误代码
is_block, err = conn:get(BUSINESS.."-BLOCK-"..ngx.var.remote_addr)  
if is_block == '1' then
    ngx.exit(403)
    goto FLAG
end
 
--查询redis中保存的ip的计数器
ip_count, err = conn:get(BUSINESS.."-COUNT-"..ngx.var.remote_addr)
 
if ip_count == ngx.null then --如果不存在,则将该IP存入redis,并将计数器设置为1、该KEY的超时时间为ip_time_out
    res, err = conn:set(BUSINESS.."-COUNT-"..ngx.var.remote_addr, 1)
    res, err = conn:expire(BUSINESS.."-COUNT-"..ngx.var.remote_addr, ip_time_out)
else
    ip_count = ip_count + 1 --存在则将单位时间内的访问次数加1
  
    if ip_count >= ip_max_count then --如果超过单位时间限制的访问次数,则添加限制访问标识,限制时间为ip_block_time
        res, err = conn:set(BUSINESS.."-BLOCK-"..ngx.var.remote_addr, 1)
        res, err = conn:expire(BUSINESS.."-BLOCK-"..ngx.var.remote_addr, ip_block_time)
    else
        res, err = conn:set(BUSINESS.."-COUNT-"..ngx.var.remote_addr,ip_count)
        res, err = conn:expire(BUSINESS.."-COUNT-"..ngx.var.remote_addr, ip_time_out)
    end
end
 
-- 结束标记
::FLAG::
local ok, err = conn:close()

这个脚本的目的很简单:一个IP如果在30秒内其访问次数达到20次则表明该IP访问频率太快了,因此将该IP封禁5分钟。同时由于计数的KEY在Redis中的超时时间设置成了30秒,所以如果两次访问间隔时间大于30秒将会重新开始计数

(2)在Nginx需要限速的location中引用上述脚本:

    location /user/ {
    set $business "USER";
    access_by_lua_file /usr/local/openresty/nginx/conf/lua/access.lua;
        proxy_redirect off;
        proxy_set_header        Host $host;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://user_224/user/;
    }

注:对于有大量静态资源文件(如:js、css、图片等)的前端页面可以设置只有指定格式的请求才进行访问限速,示例代码如下:

    location /h5 {
        if ($request_uri ~ .*\.(html|htm|jsp|json)) {
            set $business "H5";
            access_by_lua_file /usr/local/openresty/nginx/conf/lua/access.lua;
        }
        proxy_redirect off;
        proxy_set_header        Host $host;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://h5_224/h5;
    }

(3)测试:

在浏览器中访问:http://192.168.1.31:3000/user/services,可以发现能够正常浏览:

此时,登录Redis可以看到已经开始计数了:

然后使用F5反复刷新 http://192.168.1.31:3000/user/services 页面,可以发现几次之后浏览器就显示403页面了,然后一直到5分钟以后才能再次访问

参考:

评论