openresty開發系列38--通過Lua+Redis 實現動態封禁IP


openresty開發系列38--通過Lua+Redis 實現動態封禁IP

一)需求背景
為了封禁某些爬蟲或者惡意用戶對服務器的請求,我們需要建立一個動態的 IP 黑名單。
對於黑名單之內的 IP ,拒絕提供服務。

二)設計方案
實現 IP 黑名單的功能有很多途徑:
1、在操作系統層面,配置 iptables,拒絕指定 IP 的網絡請求;
2、在 Web Server 層面,通過 Nginx 自身的 deny 選項 或者 lua 插件 配置 IP 黑名單;
3、在應用層面,在請求服務之前檢查一遍客戶端 IP 是否在黑名單。

為了方便管理和共享,我們通過 Nginx+Lua+Redis 的架構實現 IP 黑名單的功能

如圖

配置nginx.conf
在http部分,配置本地緩存,來緩存redis中的數據,避免每次都請求redis

lua_shared_dict shared_ip_blacklist 8m; #定義ip_blacklist 本地緩存變量

location /ipblacklist {
    access_by_lua_file /usr/local/lua/access_by_limit_ip.lua;
    echo "ipblacklist";
}


# 編輯 /usr/local/lua/access_by_limit_ip.lua local function close_redis(red) if not red then  
        return
    end  
    --釋放連接(連接池實現) 
    local pool_max_idle_time = 10000 --毫秒 
    local pool_size = 100 --連接池大小 
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size) if not ok then ngx.say("set keepalive error : ", err) end  
end

local function errlog(...) ngx.log(ngx.ERR, "redis: ", ...) end

local function duglog(...) ngx.log(ngx.DEBUG, "redis: ", ...) end

local function getIp() local myIP = ngx.req.get_headers()["X-Real-IP"] if myIP == nil then myIP = ngx.req.get_headers()["x_forwarded_for"] end
    if myIP == nil then myIP = ngx.var.remote_addr end
    return myIP; end

local key = "limit:ip:blacklist"
local ip = getIp(); local shared_ip_blacklist = ngx.shared.shared_ip_blacklist --獲得本地緩存的最新刷新時間
local last_update_time = shared_ip_blacklist:get("last_update_time"); if last_update_time ~= nil then 
    local dif_time = ngx.now() - last_update_time if dif_time < 60 then --緩存1分鍾,沒有過期
        if shared_ip_blacklist:get(ip) then
            return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
        end
        return
    end
end

local redis = require "resty.redis"  --引入redis模塊
local red = redis:new()  --創建一個對象,注意是用冒號調用的

--設置超時(毫秒) 
red:set_timeout(1000) --建立連接 
local ip = "10.11.0.215"  
local port = 6379
local ok, err = red:connect(ip, port) if not ok then close_redis(red) errlog("limit ip cannot connect redis"); else
    local ip_blacklist, err = red:smembers(key); if err then errlog("limit ip smembers"); else
        --刷新本地緩存,重新設置
 shared_ip_blacklist:flush_all(); --同步redis黑名單 到 本地緩存
        for i,bip in ipairs(ip_blacklist) do
            --本地緩存redis中的黑名單
            shared_ip_blacklist:set(bip,true); end
        --設置本地緩存的最新更新時間
        shared_ip_blacklist:set("last_update_time",ngx.now()); end
end  

if shared_ip_blacklist:get(ip) then
    return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
end
 
        

當redis設置了密碼時代碼如下:

[root@node5 lua]# cat /usr/local/lua/access_by_limit_ip.lua

local function close_reis(red) if not red then
        return
    end
    local pool_max_idle_time = 10000
    local pool_size = 100
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size) if not ok then ngx.say("set keepalive error :", err) end
end

local function errlog(...) ngx.log(ngx.ERR, "redis: ", ...) end

local function duglog(...) ngx.log(ngx.DEBUG, "redis: ",...) end

local function getIp() local myip = ngx.req.get_headers()["X-Real-IP"] if myip == nil then myip = ngx.req.get_headers()["x_forwarded_for"] end
    if myip == nil then myip = ngx.var.remote_addr end
    return myip end

local key = "limit:ip:blacklist"
local ip = getIp(); local shared_ip_blacklist = ngx.shared.shared_ip_blacklist local last_update_time = shared_ip_blacklist:get("last_update_time"); if last_update_time ~= nil then
    local dif_time = ngx.now() - last_update_time if dif_time < 60 then
        if shared_ip_blacklist:get(ip) then
            return ngx.exit(ngx.HTTP_FORBIDDEN) end
        return
    end
end

local redis = require "resty.redis"
local red = redis:new() red:set_timeout(1000) local ip = "10.11.0.215"
local port = 6379
local ok, err = red:connect(ip,port) local count, err = red:get_reused_times() if 0 == count then ----新建連接,需要認證密碼
    ok, err = red:auth("redis123") if not ok then ngx.say("failed to auth: ", err) return
    end
elseif err then  ----從連接池中獲取連接,無需再次認證密碼
    ngx.say("failed to get reused times: ", err) return
end

if not ok then close_redis(red) errlog("limit ip cannot connect redis"); else
    local ip_blacklist, err = red:smembers(key) if err then errlog("limit ip smembers") else shared_ip_blacklist:flush_all(); for i,bip in ipairs(ip_blacklist) do shared_ip_blacklist:set(bip, true); end shared_ip_blacklist:set("last_update_time", ngx.now()); end
end

if shared_ip_blacklist:get(ip) then
    return ngx.exit(ngx.HTTP_FORBIDDEN) end

用戶redis客戶端設置:
添加黑名單IP:
sadd limit:ip:blacklist 10.11.0.148

獲取黑名單IP:
smembers limit:ip:blacklist


10.11.0.215:6379> sadd limit:ip:blacklist 10.11.0.148
10.11.0.215:6379> sadd limit:ip:blacklist 10.11.0.215

10.11.0.215:6379> smembers limit:ip:blacklist
1) "10.11.0.215"
2) "10.11.0.148"
10.11.0.215:6379> smembers limit:ip:blacklist
1) "10.11.0.215"
2) "10.11.0.148"


此方法目前只能實現手動添加黑名單IP進行IP封禁,在某些場景如:半夜如果有人惡意爬取網站服務器可能導致服務器資源耗盡崩潰或者影響業務


下面是改進后的代碼,可以實現自動將訪問頻次過高的IP地址加入黑名單封禁一段時間


nginx.conf配置部分:
location /goodslist {
        set $business "USER";
        access_by_lua_file /usr/local/lua/access_count_limit.lua;
        echo "get goods list success";
    }


lua代碼:

[root@node5 lua]# cat /usr/local/luaaccess_count_limit.lua

local function close_redis(red) if not red then
        return
    end

    local pool_max_idle_time = 10000
    local pool_size = 100
    local ok, err = red:set_keepalive(pool_max_idle_tme, pool_size) if not ok then ngx.say("set keepalive err : ", err) end
end


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("10.11.0.215", 6379) conn:set_timeout(2000) --超時時間2秒
 
--如果連接失敗,跳轉到腳本結尾
if not ok then
    --goto FLAG
 close_redis(conn) end

local count, err = conn:get_reused_times() if 0 == count then ----新建連接,需要認證密碼
    ok, err = conn:auth("redis123") if not ok then ngx.say("failed to auth: ", err) return
    end
elseif err then  ----從連接池中獲取連接,無需再次認證密碼
    ngx.say("failed to get reused times: ", err) return
end

--查詢ip是否被禁止訪問,如果存在則返回403錯誤代碼
is_block, err = conn:get(BUSINESS.."-BLOCK-"..ngx.var.remote_addr) if is_block == '1' then ngx.exit(403) close_redis(conn) 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
 
-- 結束標記
local ok, err = conn:close()


# redis的數據
10.11.0.215:6379> get USER-COUNT-10.11.0.148
"16"
10.11.0.215:6379> get USER-BLOCK-10.11.0.148
(nil)


四、總結

以上,便是 Nginx+Lua+Redis 實現的 IP 黑名單功能,具有如下優點:

1、配置簡單、輕量,幾乎對服務器性能不產生影響;

2、多台服務器可以通過Redis實例共享黑名單;

3、動態配置,可以手工或者通過某種自動化的方式設置 Redis 中的黑名單。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM