nginx+lua(OpenResty),實現訪問限制

难止汗發表於2024-06-11

  因發現平臺日誌中不定時會有同一IP傳送大量的正常請求的情況,因程式沒做請求頻率的限制,就打算使用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()
            }
        }

        header_filter_by_lua_block {
            ngx.header.content_type = 'text/html; charset=utf-8'
        }

    }
}

  指令碼說明:

    a、使用了redis的集合來儲存封禁IP的資訊

    b、access_by_lua_block 指令程式碼塊可以放在http、server、location下來控制程式碼影響範圍。

    c、request_limit引數的值和ttl的值來控制訪問頻率限制,以上程式碼是沒秒鐘50次請求。

    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

相關文章