OpenResty動態添加黑名單記錄實踐

近期,因爲某種緣由,讓我這邊再次關注到服務器身上。在咱們的服務器中,自己是使用 nginx 做爲 web 服務器的,雖然 nginx 自己支持限流和 ip 限制設置,但在動態設置黑名單中,nginx 這邊自己是作不了。這時候,就可使用到 OpenResty 了。html

OpenResty

關於 OpenResty,其實網上和自己中文主頁已經有很是詳細的介紹了,我這邊就不一一闡述了,惟一要注意的是,由於 OpenResty 自己已經包含 nginx 了,若是以前服務器上已經在使用 nginx,必須先把當前版本的 nginx 卸載掉,而後再安裝 OpenResty,因此須要先把原來的 nginx 服務停掉而且備份以前的配置文件,而後再進行安裝,我這邊是經過 yum 進行安裝的,具體使用的指令nginx

# 刪除本來的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 腳本實現動態黑名單配置。git

動態黑名單腳本

首先,由於實踐上是使用 OpenResty 以及下面的 redis 組件,若是自己對 lua 和 redis 不太熟悉的話,須要先基本瞭解下相關的知識,這裏能夠去查閱下 OpenResty 中關於 redis 組件和 nginx 組件的相關說明。github

而後,我這邊是參考起航天空[1]博主這篇文章的作法,而且作了某些小調整。web

在這個過程當中,博主給到我這邊許多意見和見解,而且很耐心地聽取我這邊的一些建議和給到解答,對於本人在運維方面上,也給到一些其餘的解決方法,在此表示十分的感謝。redis

下面就直接發下處理的代碼,首先是配置代碼:shell

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;
複製代碼

這裏跟例子中的配置沒什麼明顯的差異,分別來講明一下各個配置的含義:apache

  • 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 就行。bash

接着是這個具體的 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] blog.mintrumpet.fun/ 或者 蘆葦科技 來跟我作關於技術上的交流,那麼今天就到此結束吧。Enjoy Coding!


小喇叭

廣州蘆葦科技Java開發團隊

蘆葦科技-廣州專業互聯網軟件服務公司

抓住每一處細節 ,創造每個美好

關注咱們的公衆號,瞭解更多

想和咱們一塊兒奮鬥嗎?lagou搜索「 蘆葦科技 」或者投放簡歷到 server@talkmoney.cn 加入咱們吧

關注咱們,你的評論和點贊對咱們最大的支持


  1. 起航天空,www.qhjack.cn/ ↩︎

  2. 小喇叭的小屋,blog.mintrumpet.fun/ ↩︎

相關文章
相關標籤/搜索