背景
前阵子遇上了类似ddos的攻击, 为了防止某恶意用户多次对服务器端口进行攻击,我们需要建立一个动态的 IP 黑名单。对于黑名单之内的 IP ,拒绝提供服务。
实现 IP 黑名单的功能有很多途径:
在操作系统层面,配置 iptables,拒绝指定 IP 的网络请求;
在 Web Server 层面,通过 Nginx 自身的 deny 选项 或者 lua 插件 配置 IP 黑名单;
在应用层面,在请求服务之前检查一遍客户端 IP 是否在黑名单。
为了方便管理和共享,我们通过 Nginx+Lua+Redis 的架构实现 IP 黑名单和 IP 白名单的功能。
本方法不用openresty方式, 如果用的openresty会更简便, 但是由于本机已经安装了nginx, 则选择后加lua和nginx-lua-module的方式搭配使用
在已经安装好nginx、lua 和nginx的lua模块后, 还需要下载一个openresty的lua-resty-redis模块, 实现操作redis功能的一些库
安装lua-resty-redis模块
方法1:
#下载包
wget https://github.com/openresty/lua-resty-redis/archive/refs/tags/v0.29.tar.gz
#解压包
tar -zxvf v0.29.tar.gz
#将解压出来的包中的lib/resty解压到nginx文件夹下的lua文件夹
cp -r lua-resty-redis-0.29/lib/resty $NGINX_INSTALL_PATH/lua/
方法2:
#git clone
git clone https://github.com/openresty/lua-resty-redis.git
#将包中的lib/resty解压到nginx文件夹下的lua文件夹
cp -r lib/resty $NGINX_INSTALL_PATH/lua/
$NGINX_INSTALL_PATH 为nginx的目录, 复制到这个目录后续nginx的配置文件需要引入这个目录的.lua文件
配置nginx.conf
这里用一个默认的nginx配置default.conf举例。 需要指明lua库的路径, 加在server{}外部
lua_package_path "/etc/nginx/lua/?.lua;;";
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html;
# Add index.php to the list if you are using PHP
index index.html index.htm index.nginx-debian.html;
server_name _;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
location /lua {
set $business "lua"; #加这个的目的是为了知道存在redis的数据是哪个location的,也可以不加
access_by_lua_file /etc/nginx/ip.lua; #lua脚本位置
default_type 'text/html';
content_by_lua 'ngx.say("hello, lua!~~~~~~~~")';
}
}
/etc/nginx/lua/?.lua
是引入lua的拓展包, 其中的/etc/nginx/lua
是lua的拓展包目录
set $business "lua"
是接下来的lua脚本传入的类似标识区分的字符, 这样做可以给redis缓存加一个标识, 有效区分不同项目或功能
access_by_lua_file /etc/nginx/ip.lua;
值的是执行的lua脚本
lua脚本
下面是lua脚本ip.lua, 文件位置位于/etc/nginx/
-- 封禁IP时间(秒)
local ip_block_time=300
-- 指定ip访问频率时间段(秒)
local ip_time_out=10
-- 指定ip访问频率计数最大值(秒)
local ip_max_count=1
-- nginx的location中定义的业务标识符,也可以不加,不过加了后方便区分
local BUSINESS = ngx.var.business
-- ip白名单(不需要拦截)
local whiteList = {"127.111.111.111"}
-- 连接redis
local redis = require "resty.redis"
local conn = redis:new()
ok, err = conn:connect("127.0.0.1", 6379)
-- 超时时间2秒
conn:set_timeout(2000)
-- 如果连接失败,跳转到脚本结尾
if not ok then
goto FLAG
end
-- 判断ip是否在白名单内, 如果在则不做记录和拦截
-- 方法1 直接往whiteList添加元素, 键值对的形式, 比如whiteList["127.0.0.1"] = 1
-- if whiteList[ngx.var.remote_addr] ~= nil then
-- goto FLAG
-- end
-- 方法二 利用循环判断一维数组值是否存在, whiteList的定义为{"127.111.111.111"}
for k,v in ipairs(whiteList) do
if v == ngx.var.remote_addr then
goto FLAG
end
end
-- 查询ip是否被禁止访问,如果存在则返回403错误代码
is_block, err = conn:get(BUSINESS.."-BLOCK-"..ngx.var.remote_addr)
if is_block == '1' then
ngx.exit(403)
goto FLAG
end
-- 查询redis中保存的ip的计数器
ip_count, err = conn:get(BUSINESS.."-COUNT-"..ngx.var.remote_addr)
-- 如果不存在,则将该IP存入redis,并将计数器设置为1、该KEY的超时时间为ip_time_out
if ip_count == ngx.null then
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
-- 存在则将单位时间内的访问次数加1
ip_count = ip_count + 1
-- 如果超过单位时间限制的访问次数,则添加限制访问标识,限制时间为ip_block_time
if ip_count >= ip_max_count then
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
-- 结束标记
::FLAG::
local ok, err = conn:close()
按照脚本规定, 十秒内访问一次, 第二次则直接返回403, 测试效果如预期; 5分钟后重新访问则又可访问一次
也可以通过服务端的redis客户端, 查看当前的被记录的ip