一、背景
為了有效封禁某些爬蟲或惡意用戶對服務(wù)器的請求,或者只允許某些來自指定白名單IP用戶訪問服務(wù)器,我們需要建立一個動態(tài)的 IP 黑/白名單機制。對于被列入黑/白名單的 IP 地址,我們將拒絕/同意為其提供服務(wù)。
二、架構(gòu)選擇
實現(xiàn) IP 黑/白名單的功能有多種途徑:
- 操作系統(tǒng)層面:通過配置
iptables 來拒絕/同意指定 IP 的網(wǎng)絡(luò)請求; - Web Server 層面:利用 Nginx 自身的
deny 指令或者 Lua 插件來配置 IP 黑/白名單; - 應(yīng)用層面:在請求服務(wù)之前檢查客戶端 IP 是否在黑名單中。
為了便于管理和共享,我們選擇了 Nginx + Lua + Redis/Txt文本文件 的架構(gòu)來實現(xiàn) IP 黑/白名單功能。架構(gòu)圖如下:
三、實現(xiàn)步驟
1. 安裝 Nginx + Lua 模塊
推薦使用 OpenResty,這是一個集成了各種 Lua 模塊的 Nginx 服務(wù)器發(fā)行版,方便快速部署 Lua 支持。
2. 安裝并啟動 Redis 服務(wù)器 / 或者利用Txt文本文件來儲存IP黑/白名單
確保 Redis 已安裝并正常運行。本次通過 Docker 快速啟動 Redis:
docker run -itd --name redis -p 6379:6379 redis
然后在 Redis 中創(chuàng)建一個集合用于存儲黑/白名單 IP:
redis-cli
SADD ip_blacklist "192.168.56.1"
3. 配置 Nginx
3.1 修改 nginx.conf
在 Nginx 配置文件中添加以下內(nèi)容,分配一塊共享內(nèi)存空間用于緩存 IP 黑/白名單,并指定 Lua 腳本位置:
http {
# 分配 1M 共享內(nèi)存用于存儲 IP 黑名單
lua_shared_dict ip_blacklist 1m;
server {
listen 80;
server_name localhost;
location = /ipblacklist {
access_by_lua_file lua/ip_blacklist.lua;
default_type text/html;
content_by_lua '
ngx.say("<p>hello, lua</p>")
';
}
}
}
4. 編寫 Lua 腳本
將以下 Lua 腳本保存為 ip_blacklist.lua,該腳本會定期從 Redis 或讀取 Txt文本文件 獲取最新的黑/白名單數(shù)據(jù),并更新本地緩存。
-- Redis服務(wù)器地址
local redis_host = "your.redis.server.here"
-- Redis服務(wù)器端口
local redis_port = 6379
-- Redis連接超時時間(毫秒),不要設(shè)置得太高!
local redis_connect_timeout = 100
-- 要檢查的黑名單集合的鍵名
local redis_key = "ip_blacklist"
-- 緩存查找的有效時間(秒)
local cache_ttl = 60
-- 結(jié)束配置部分
-- 獲取客戶端IP地址
local ip = ngx.var.remote_addr
-- 獲取共享內(nèi)存中的ip_blacklist
local ip_blacklist = ngx.shared.ip_blacklist
-- 獲取上次更新的時間戳
local last_update_time = ip_blacklist:get("last_update_time")
-- 只有在cache_ttl秒之后才從Redis更新ip_blacklist:
if last_update_time == nilor last_update_time < (ngx.now() - cache_ttl) then
-- 引入redis模塊
local redis = require"resty.redis";
local red = redis:new();
-- 設(shè)置Redis連接超時時間
red:set_timeout(redis_connect_timeout);
-- 嘗試連接到Redis
local ok, err = red:connect(redis_host, redis_port);
ifnot ok then
-- 如果連接失敗,記錄調(diào)試日志
ngx.log(ngx.DEBUG, "Redis connection error while retrieving ip_blacklist: " .. err);
else
-- 從Redis獲取新的ip_blacklist數(shù)據(jù)
local new_ip_blacklist, err = red:smembers(redis_key);
if err then
-- 如果讀取失敗,記錄調(diào)試日志
ngx.log(ngx.DEBUG, "Redis read error while retrieving ip_blacklist: " .. err);
else
-- 替換本地存儲的ip_blacklist為最新的值:
ip_blacklist:flush_all();
for index, banned_ip inipairs(new_ip_blacklist) do
ip_blacklist:set(banned_ip, true);
end
-- 更新時間戳
ip_blacklist:set("last_update_time", ngx.now());
end
end
end
-- 檢查客戶端IP是否在黑名單中
if ip_blacklist:get(ip) then
-- 如果在黑名單中,記錄調(diào)試日志并拒絕訪問
ngx.log(ngx.DEBUG, "Banned IP detected and refused access: " .. ip);
return ngx.exit(ngx.HTTP_FORBIDDEN);
end
5. 測試與生效
完成以上步驟后,重新加載 Nginx 配置以使更改生效:
nginx -s reload
此時,如果訪問者的 IP 在黑/白名單中,將被拒絕/同意訪問,拒絕時返回 403 Forbidden。
四、總結(jié)
通過上述方法,我們實現(xiàn)了基于 Nginx + Lua + Redis/Txt文本文件 的 IP 黑/白名單功能,具有以下優(yōu)點:
- 輕量高效:配置簡單,幾乎不增加服務(wù)器性能負擔;
- 集中管理:多臺服務(wù)器可以通過同一個 Redis 實例 或 TXT文本文件 共享黑/白名單數(shù)據(jù);
- 動態(tài)更新:可以手動、第三方程序或通過自動化方式更新 Redis/Txt文本文件 中的黑/白名單,無需重啟服務(wù)即可生效;
- 實時校驗:對于非白名單中的請求,可以將其轉(zhuǎn)向到一個身份驗證網(wǎng)站,訪問者回答正確時,就可以將其IP加入 Redis 實例 或 TXT文本文件,之后就可以立即訪問了。
這種方案非常適合用于需要動態(tài)控制訪問權(quán)限的場景,如反爬蟲、安全防護等。
閱讀原文:原文鏈接
該文章在 2025/8/6 18:50:32 編輯過