nginx+lua(OpenResty),实现访问限制

  因发现部署在云平台上的业务日志中,不定时会有同一IP发送大量的正常请求的情况,导致服务器资源占用突然增高,由于程序端没做请求频率的限制,云服务上没有waf,故使用nginx+lua(OpenResty)+redis来做访问控制,发现请求频率高的IP,直接封掉,禁止访问。

一、部署OpenResty

  1、安装工具和依赖

 yum -y install  wget  vim  gcc pcre-devel  openssl-devel

 

  2、从 https://github.com/openresty/openresty,下载OpenResty,这里使用openresty-1.25.3.1版本。

wget https://github.com/openresty/openresty/releases/download/v1.25.3.1/openresty-1.25.3.1.tar.gz

 

  3、解压、配置、编译、安装

tar zxf  openresty-1.25.3.1.tar.gz
cd openresty-1.25.3.1
./configure --prefix=/data/openresty
gmake && gmake install

 

二、部署redis

  用redis来存储被封的IP,当然也可以考虑使用其他的方式,这里使用redis,假设redis密码为123456,传送阵:部署redis

 

三、编写lua脚本,启动redis

  1、在nginx配置文件中location模块里加入lua代码

    vim /data/openresty/nginx/conf/nginx.conf

http {

    .... 

    #创建一个共享内存区域来存储IP访问频率
    lua_shared_dict limit_req_store 10m;
    
    server {
        listen       80;
        
         ....
    
        location / {
            root   html;
            index  index.html index.htm;
            #添加lua脚本,access_by_lua_block指令
            access_by_lua_block {
                ngx.header.content_type = "text/plain; charset=utf-8"
                local request_limit = 50   -- 定义每个IP的请求频率限制变量
                local ttl = 1    -- 定义缓存过期时间变量,单位秒
                local redis_passwd = "123456" -- 定义redis密码
                local redis_host = "192.168.1.11" -- 定义redis服务器地址
                local redis_port = 6379  -- 定义redis端口信息
                local redis_set_key = "limit_ip"  -- 定义redis集合的key信息

                local redis = require "resty.redis"
                local red = redis:new()
                red:set_timeout(1000)
                local ok, err = red:connect(redis_host, redis_port) 
                if not ok then  
                    ngx.log(ngx.ERR,"连接redis出错: ", err)
                    return
                end
                ok , err = red:auth(redis_passwd)
                if not ok then
                    ngx.log(ngx.ERR,"redis授权失败:", err)
                    return
                end
                
                local cardinality ,err = red:scard(redis_set_key) 
                if cardinality == 0 then  
                    local my_limit = ngx.shared.my_limit_req_store
                    local client_ip = ngx.var.remote_addr
                    local request_count, _ = my_limit:get(client_ip)
                    if not request_count  then
                        request_count = 1
                    else 
                        request_count = request_count + 1
                    end
                    my_limit:set(client_ip,request_count,ttl)                
                    
                    if request_count > request_limit then  
                        red:sadd(redis_set_key,client_ip)
                        ngx.status = ngx.HTTP_TOO_MANY_REQUESTS
                        ngx.log(ngx.ERR,"该IP请求频率过高,已被禁止访问!",client_ip)
                        ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
                    end
                else
                    local client_ip = ngx.var.remote_addr
                    local is_member, err = red:sismember(redis_set_key, client_ip)  
                    if err then
                        ngx.log(ngx.ERR,"检查IP是否存在于redis中出错: ", err)
                        return
                    end
                    if is_member == 0 then  
                        local my_limit = ngx.shared.my_limit_req_store
                        local client_ip = ngx.var.remote_addr
                        local request_count, _ = my_limit:get(client_ip)
                        if not request_count  then
                            request_count = 1
                        else
                            request_count = request_count + 1
                        end
                        my_limit:set(client_ip,request_count,ttl)

                        if request_count > request_limit then
                            red:sadd(redis_set_key,client_ip)
                            ngx.status = ngx.HTTP_TOO_MANY_REQUESTS
                            ngx.log(ngx.ERR,"该IP请求频率过高,已被禁止访问!",client_ip)
                            ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
                        end
                    else 
                        ngx.status = ngx.HTTP_FORBIDDEN
                        ngx.log(ngx.ERR,"该IP已被禁止访问!",client_ip)
                        ngx.exit(ngx.HTTP_FORBIDDEN)
                    end   
                end  
                red:close()
            }
        }
        #配置content_type
        header_filter_by_lua_block {
            local content_type = ngx.header.content_type
            if ngx.var.uri:match("%.css$") then
                ngx.header.content_type = 'text/css; charset=utf-8'
            elseif ngx.var.uri:match("%.js$") then
                ngx.header.content_type = 'application/javascript; charset=utf-8'
            elseif ngx.var.uri:match("%.html$") or ngx.var.uri == "/" then
                ngx.header.content_type = 'text/html; charset=utf-8'
            else
                ngx.header.content_type = content_type
            end
        }
  }
}

    脚本说明:

      a、使用了redis的集合来存储封禁IP的信息

      b、access_by_lua_block 指令代码块可以放在http、server、location等模块下,来控制代码影响范围。

      c、request_limit参数的值和ttl的值来控制访问频率限制,以上代码是限制每1秒钟50次请求,超过限制后,客户端IP将被禁止请求,直接报错403。

      d、使用一个静态的html页面(没有css、js等代码)进行测试,请求1次,会执行2次access_by_lua_block指令,未找出具体原因,因此request_limit的值如果为20,ttl为1,那么该测试在1秒内限制请求10次。

      e、该脚本对IP地址进行永久封禁(除非redis的数据丢失),如果想要实现限制时长功能,可以用其他方式存储,该方式的解封方式就只能操作redis来删除数据。

 

  2、启动nginx

/data/openresty/nginx/sbin/nginx

 

posted @ 2024-06-11 14:46  难止汗  阅读(31)  评论(0编辑  收藏  举报