近期,由于某种原因,让我这边再次关注到服务器身上。在我们的服务器中,本身是使用 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;

这里跟例子中的配置没什么明显的差别,分别来说明一下各个配置的含义:

这个依据个人喜好和需求来设定,一般情况下控制好 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!