Openresty + Lua + Redis 实现动态IP黑名单

在 Nginx 中屏蔽特定 IP 地址是提升服务器安全性的有效方式,可以防止恶意访问和攻击。

配置deny指令来屏蔽IP

Nginx 主要通过 deny 指令来屏蔽 IP,可以根据需要在不同的配置层级(http、server、location)中添加规则。

  • 单个 IP deny 192.168.1.100; 屏蔽一个具体的 IP 地址16
  • IP 段 (CIDR) deny 192.168.1.0/24; 屏蔽一个网段的所有 IP126
  • 所有 IP deny all; 屏蔽所有访问,通常与 allow 配合使用

配置位置:

  • http{} 块:对 Nginx 所有服务生效6。

  • server{} 块:只对指定的虚拟主机生效26。

  • location{} 块:只对匹配的 URL 路径生效。

示例:在 server 块中屏蔽 IP

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
server {
    listen 80;
    server_name example.com;
    
    # 屏蔽特定IP和IP段
    deny 192.168.1.100;
    deny 10.0.0.0/8;
    deny 47.92.79.0/24; # 屏蔽整个IP段:cite[2]
    allow all; # 允许其他所有IP。注意顺序,通常先deny后allow:cite[2]:cite[3]
    
    location / {
        # 你的其他配置...
    }
    
    # 可以针对特定location进行屏蔽
    location /admin {
        deny 192.168.1.101;
        allow all;
        # 其他admin配置...
    }
}

使用外部配置文件(推荐管理大量IP)

当需要屏蔽的 IP 地址很多时,使用独立的配置文件可以提高可读性和可维护性

  1. 创建黑名单配置文件:例如,在 Nginx 配置目录(如 /etc/nginx/)下创建一个 blockips.conf 文件,并添加要屏蔽的 IP。

文件内容示例:

1
2
3
4
5
# Block malicious IPs
deny 123.123.123.123;
deny 111.111.0.0/16;
deny 222.222.222.0/24;
# ... 其他需要屏蔽的IP
  1. 在主配置文件中引入:在 nginx.conf 的 http, server 或 location 块中包含这个文件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
http {
    # 其他http配置...
    
    # 全局引入(对所有server生效)
    include /etc/nginx/blockips.conf; 
    
    server {
        listen 80;
        server_name example.com;
        
        # 或者仅在此server引入
        # include /etc/nginx/blockips.conf;
        
        location / {
            # 或者仅在此location引入
            # include /etc/nginx/blockips.conf;
            # 其他配置...
        }
    }
}
  • 规则顺序:Nginx 的 allow 和 deny 指令遵循顺序匹配,第一条匹配的规则生效。例如,如果先写了 allow all;,后面的 deny 规则就会失效。因此,通常将具体的 deny 规则放在前面,最后再写 allow all;

高级用法:使用 geo 模块

geo 模块可以更灵活地定义 IP 地址块并将其映射到变量,适合管理大量需要屏蔽的 IP 或网段,有时也用于防范 DDoS 攻击

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
http {
    # 定义一个名为$block_ip的变量,默认为0(不屏蔽)
    geo $block_ip {
        default 0; # 默认值
        # 将需要屏蔽的IP或网段值设为1
        192.168.1.100 1;
        10.0.0.0/8 1;
        47.92.79.0/24 1; # 屏蔽整个IP段:cite[2]
        # 可以从文件导入IP列表
        # include /etc/nginx/block-ips.geo;
    }
    
    server {
        listen 80;
        server_name example.com;
        
        # 如果$block_ip变量值为1,则返回403
        if ($block_ip) {
            return 403;
        }
        
        location / {
            # 其他配置...
        }
    }
}
  • 结合 Fail2ban 自动封禁

对于频繁的恶意攻击(如暴力破解、扫描),可以配置 Fail2ban 这类工具动态分析 Nginx 日志,并自动将恶意 IP 添加到 Nginx 的屏蔽配置中,实现动态的自动封禁

Openresty + Lua + Redis 实现动态 IP 黑名单

  1. 安装并启动 Redis 服务器
1
docker run -itd --name redis -p 6379:6379 redis

然后在 Redis 中创建一个集合用于存储黑名单 IP:

1
2
redis-cli
SADD ip_blacklist 192.168.1.1
  1. 配置 nginx.conf

在 Nginx 配置文件中添加以下内容,分配一块共享内存空间用于缓存 IP 黑名单,并指定 Lua 脚本位置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
http {
    # 分配 1M 共享内存用于存储 IP 黑名单
    lua_shared_dict ip_blacklist 1m;

    server {
        listen 80;
        server_name localhost;

        location = /ipblacklist {
            default_type text/html;
            content_by_lua_file lua/ip_blacklist.lua;
        }
    }
}
  1. lua脚本存储ip黑名单

用 ip_blacklist.lua 脚本实现,定期从 Redis 获取最新的黑名单数据,并更新本地缓存。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
-- Redis服务器地址
local redis_host = "172.17.0.3"
-- Redis服务器端口
local redis_port = 6379

-- Redis连接超时时间(毫秒),不要设置得太高!
local redis_connect_timeout = 100

-- 要检查的黑名单集合的键名
local redis_key = "ip_blacklist"

-- 缓存查找的有效时间(秒)
local cache_ttl = 60

-- 结束配置部分

-- 获取客户端IP地址
local ip = ngx.var.remote_addr
ngx.log(ngx.INFO, "remote addr IP : " .. ip);
-- 获取共享内存中的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 == nil or last_update_time < (ngx.now() - cache_ttl) then

    -- 引入redis模块
    local redis = require"resty.redis";
    local red = redis:new();

    -- 设置Redis连接超时时间
    red:set_timeout(redis_connect_timeout);

    -- 尝试连接到Redis
    local ok, err = red:connect(redis_host, redis_port);
    if not ok then
        -- 如果连接失败,记录调试日志
        ngx.log(ngx.DEBUG, "Redis connection error while retrieving ip_blacklist: " .. err);
    else
        -- local res, err = red:spop(redis_key);
        -- ngx.log(ngx.INFO, "spop ip_blacklist: ", err, res);

        -- 从Redis获取新的ip_blacklist数据
        local new_ip_blacklist, err = red:smembers(redis_key);
        if err then
            -- 如果读取失败,记录调试日志
            ngx.log(ngx.DEBUG, "Redis read error while retrieving ip_blacklist: " .. err);
        else
            -- 替换本地存储的ip_blacklist为最新的值:
            ip_blacklist:flush_all();
            for index, banned_ip in ipairs(new_ip_blacklist) do
                ngx.log(ngx.INFO, "new_ip_blacklist banned_ip: ", banned_ip, index);
                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
    -- 如果在黑名单中,记录调试日志并拒绝访问
    ngx.log(ngx.DEBUG, "Banned IP detected and refused access: " .. ip);
    return ngx.exit(ngx.HTTP_FORBIDDEN);
end
ngx.say("<p>hello, world!</p>")

加载配置后访问,如果在ip黑名单中,则返回403 Forbidden:

1
2
3
4
5
6
7
8
curl 'localhost/ipblacklist'
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>openresty/1.25.3.2</center>
</body>
</html>

否则返回正常页面:

1
2
curl 'localhost/ipblacklist'
<p>hello, world!</p>

参考