0%

OpenResty + Redis实现IP黑名单

Nginx使用OpenResty + lua + Redis实现一个IP黑名单机制,可以在请求到达应用之前高效地过滤恶意IP。

实现原理

OpenResty是Nginx的一个分支,可以让我们在nginx中调用lua脚本,同时能充分地利用nginx的非阻塞IO模型,在nginx高效地实现一些业务逻辑。

实现IP黑名单机制原理很简单:每次请求到来时,去redis中查询ip是否在黑名单中,如果在黑名单则返回错误。

使用同样的原理也可以实现IP白名单。

Redis数据结构

保存IP黑名单,可以考虑的Redis存储方案有:

  • 字符串
  • 集合
  • 哈希
  • 位图

从内存占用方面看
使用字符串、集合、哈希来存储IP时,占用的内存根据IP数量递增,而位图需要固定占用512MB内存(只考虑IPv4,总共2^32个≈42亿个IP地址)。如果不需要屏蔽这么多IP地址,占用512兆内存会比较浪费。

从性能方面看
常用的两个操作增加IP、解除IP,在使用字符串、集合、哈希、位图时,时间复杂度均为O(1)。但在统计IP数量时,集合、哈希的时间复杂度为O(1),而字符串、位图为O(N),性能较差。

从扩展性看
在实际应用场景中,为了减少误判带来的影响,通常会设置黑名单过期时间,例如10分钟后自动解除。在Redis中,使用集合、哈希、位图时,均不能给指定的IP设置过期时间(不过可以把过期时间存储在哈希的值中)。

为了简单起见,本文使用字符串来存储IP,redis的key为IP,值可以随便设置(用于扩展)。

Nginx配置

server内加上access_by_lua_file并指定lua脚本位置即可:

1
2
3
4
5
server{

access_by_lua_file '/usr/local/openresty/nginx/block.lua';
# .....
}

access_by_lua_file指令也可以放到httplocation中,实现整个http服务或者指定URL的屏蔽。

Lua代码

创建block.lua文件,代码如下:

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
local redis = require("resty.redis");

local redis_instance = redis:new();
redis_instance:set_timeout(10000)

-- 连接redis
local ok,err = redis_instance:connect('127.0.0.1', 6379)
if not ok then
return ngx.exit(500)
end

-- 获取客户端ip
local ip = ngx.var.remote_addr;

-- 切换Redis数据库,使用一个单独的Redis数据库来存储黑名单IP
redis_instance:select(13)

-- 判断ip是否在黑名单,如果在返回403 Forbidden
val, err = redis_instance:get(ip)

-- 查询完之后放入连接池
redis_instance:set_keepalive(100000, 5)

if val ~= ngx.null then
return ngx.exit(403);
end

-- 校验通过
return true

保存后重启Nginx即可:

1
sudo openresty -s reload

对性能的影响

在LNMP的环境下,经过简单测试,使用lua脚本之前QPS为1640,使用之后QPS下降到1555,对性能有轻微影响。

性能影响的大小与具体的业务有关,测试结果仅供参考。

如果对性能有更高的要求,可以把IP地址缓存在lua,例如lua-resty-mlcache