近期,由于某种原因,让我这边再次关注到服务器身上。在我们的服务器中,本身是使用 nginx 作为 web 服务器的,虽然 nginx 本身支持限流和 ip 限制设置,但在动态设置黑名单中,nginx 这边本身是做不了。这时候,就可以使用到 OpenResty 了。
OpenResty
关于 OpenResty,其实网上和本身中文主页已经有非常详细的介绍了,我这边就不一一阐述了,唯一要注意的是,因为 OpenResty 本身已经包含 nginx 了,如果之前服务器上已经在使用 nginx,必须先把当前版本的 nginx 卸载掉,然后再安装 OpenResty,所以需要先把原来的 nginx 服务停掉并且备份之前的配置文件,然后再进行安装,我这边是通过 yum 进行安装的,具体使用的指令
# 删除原本的nginx,记得先备份配置
systemctl disable nginx.service
rm -rf /usr/lib/systemd/system/nginx.service
yum erase nginx
# 安装 yum-utils
yum install yum-utils
# yum 安装 openresty
yum install openresty
# 配置 nginx profile PATH
PATH=/usr/local/openresty/nginx/sbin:$PATH
export PATH
# 指定配置
nginx -c /usr/local/openresty/nginx/conf/nginx.conf
至此,关于 OpenResty 的简单准备就完成了,下面我们来看下如何通过 lua 脚本实现动态黑名单配置。
动态黑名单脚本
首先,因为实践上是使用 OpenResty 以及下面的 redis 组件,如果本身对 lua 和 redis 不太熟悉的话,需要先基本了解下相关的知识,这里可以去查阅下 OpenResty 中关于 redis 组件和 nginx 组件的相关说明。
然后,我这边是参考起航天空^1博主这篇文章的做法,并且做了某些小调整。
在这个过程中,博主给到我这边许多意见和看法,并且很耐心地听取我这边的一些建议和给到解答,对于本人在运维方面上,也给到一些其他的解决方法,在此表示十分的感谢。
下面就直接发下处理的代码,首先是配置代码:
set $redis_service "127.0.0.1";
set $redis_port 6380;
set $redis_db 0;
# 1 second 50 query
set $black_count 50;
set $black_rule_unit_time 1;
set $black_ttl 3600;
set $auto_blacklist_key blackkey;
这里跟例子中的配置没什么明显的差别,分别来说明一下各个配置的含义:
- redis_service:redis 服务器 ip 地址
- redis_port:redis 服务器端口
- redis_db:所使用的redis db
- black_count:拉黑限制的最大访问次数
- black_rule_unit_time:拉黑限制次数的保存时间,即保存访问次数的 kv 的 ttl
- black_ttl:黑名单的存活时间,因为我这里是永久存货,所以没使用到
- auto_blacklist_key:kv 的部分 key
这个依据个人喜好和需求来设定,一般情况下控制好 black_count 和 black_rule_unit_time 就行。
接着是这个具体的 lua 脚本代码,其中大部分也是按照例子中的来:
local redis_service = ngx.var.redis_service
local redis_port = tonumber(ngx.var.redis_port)
local redis_db = tonumber(ngx.var.redis_db)
local black_count = tonumber(ngx.var.black_count)
local black_rule_unit_time = tonumber(ngx.var.black_rule_unit_time)
local cache_ttl = tonumber(ngx.var.black_ttl)
local remote_ip = ngx.var.remote_addr
-- 计数
function my_count(redis, status_key, count_key)
local key = status_key
local key_connect_count = count_key
local Status = redis:get(key)
local count = redis:get(key_connect_count)
if Status ~= ngx.null then
-- 状态为connect 且 count不为空 且 count <= 拉黑次数
if (Status == "Connect" and count ~= ngx.null and tonumber(count) <= black_count) then
-- 再读一次
count = redis:incr(key_connect_count)
ngx.log(ngx.ERR, "count:", count)
if count ~= ngx.null then
if tonumber(count) > black_count then
redis:del(key_connect_count)
redis:set(key,"Black")
-- 永久封禁
-- Redis:expire(key,cache_ttl)
else
redis:expire(key_connect_count,black_rule_unit_time)
end
end
else
ngx.log(ngx.ERR,"The visit is blocked by the blacklist because it is too frequent. Please visit later.")
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
else
local count = redis:get(key)
if count == ngx.null then
redis:del(key_connect_count)
end
redis:set(key,"Connect")
redis:set(key_connect_count,1)
redis:expire(key,black_rule_unit_time)
redis:expire(key_connect_count,black_rule_unit_time)
end
end
-- 读取token
local token
local header = ngx.req.get_headers()["Authorization"]
if header ~= nil then
token = string.match(header, 'token (%x+)')
end
local redis_connect_timeout = 60
local redis = require "resty.redis"
local Redis = redis:new()
local auto_blacklist_key = ngx.var.auto_blacklist_key
Redis:set_timeout(redis_connect_timeout)
local RedisConnectOk,ReidsConnectErr = Redis:connect(redis_service,redis_port)
local res = Redis:auth("password");
if not RedisConnectOk then
ngx.log(ngx.ERR,"ip_blacklist connect Redis Error :" .. ReidsConnectErr)
else
-- 连接成功
Redis:select(redis_db)
local key = auto_blacklist_key..":"..remote_ip
local key_connect_count = auto_blacklist_key..":key_connect_count:"..remote_ip
my_count(Redis, key, key_connect_count)
if token ~= nil then
local token_key, token_key_connect_count
token_key = auto_blacklist_key..":"..token
token_key_connect_count = auto_blacklist_key..":key_connect_count:"..token
my_count(Redis, token_key, token_key_connect_count)
end
end
因为在 lua 的实践上本人也是属于一个新手的级别,所以在结构性上都有很明显的问题,这里先留一个坑。
先解释下这段代码,因为我这边是从 ip 及 token(访问凭证) 入手来控制,所以先将参考例子中的计数整合在一个 function 当中。function 里头原本例子中是使用 set
方法来做加一操作的,所以在大量请求进入的时候,会产生一个同步问题,所以我这边稍微改造一下,使用 incr
来做一个自增操作,并且在进入方法时就获取计数值并判断计数值大小是否超过阈值 black_count
,一次来规避大量请求时产生的问题。
接着是下面获取 token 中,我是根据应用中使用的凭证做法,从头部获得 Authorization,并且从中截取来拿到 token,如果 token 为空,就证明不需要经过 token 的计数处理。
最后是连接并调用函数了,这里没什么要说明的地方,主要说明在定义 function 和使用 function 的顺序需要注意一下。
然后是实配到 nginx 的 conf 当中了:
server {
listen 80;
server_name blog.mintrumpet.fun;
root /~/public;
# 加载配置文件
include /etc/nginx/conf.d/blacklist_params;
# 指定请求中需要执行的 lua 脚本
access_by_lua_file /etc/nginx/conf.d/ip_blacklist.lua;
location / {
}
error_log /etc/nginx/conf.d/log/error.log;
access_log /etc/nginx/conf.d/log/access.log;
}
以上,配置就完成了,在 console 中重启下 nginx nginx -s reload
,就可以实现动态添加黑名单的需要了。至于对于添加到黑名单的 ip 及 token,需要怎么做下一步的处理,这边就给服务器下的具体应用来处理,在这里不阐述。
测试
本人在过程中是使用个人的服务器里的博客,以及 apache bench 工具来做测试的。
先测试一个不带 token(游客) 的例子,访问一个静态文件,
我以10秒50次作为限制,首先是4个并发访问40次:
ab -n 40 -c 4 http://blog.mintrumpet.fun/dist/music.js
在执行结果中,可以看到40个请求都顺利完成。
再看下 redis 下的值,
还行,还没超过限制的大小。
接着4个并发访问100次:
ab -n 100 -c 4 http://blog.mintrumpet.fun/dist/music.js
从结果可以看到,里面有49条请求访问失败,显然都被转到403了。
再看下 redis 下的值,
很显然,我的ip被屏蔽了,接着去访问的时候,提示403错误,OK,目的达到了。
接着来测试一个 token 的例子,同样也是访问一个静态文件,也是4个并发访问100次:
ab -n 100 -c 4 -H "Authorization:token 87BF813C6DDB9C01D4525F47908D4C9F" http://blog.mintrumpet.fun/dist/music.js
结果也是一样,这个token被屏蔽了,后续的访问也转发到403。
至此,整个测试就到此结束了,可以看到无论是游客访问,还是身份访问,都能起到同样的效果。
小结
其实这种做法只能用作一般的情况,而且在配置及编写的脚本文件中,还有很多需要改善的地方,在此我也只是有样学样,各位读者如果有更好的做法和方案,可以在下面的讨论中告诉读者我。同时在跟起航天空的博主交流的时候,他也告诉我其他做限流和拦截的处理来抵制攻击和监控,例如 fail2ban、OSSEC等。而 nginx 自身也提供一系列限流措施,有兴趣的各位可以自行学习。
至此,关于 OpenResty 的实践就到这里结束了,大家可以关注下我的博客^2 http://blog.mintrumpet.fun/ 或者 芦苇科技 来跟我做关于技术上的交流,那么今天就到此结束吧。Enjoy Coding!