每個後端都應該瞭解的OpenResty入門以及閘道器安全實戰

waynaqua發表於2023-10-15

每個後端都應該瞭解的OpenResty入門以及閘道器安全實戰

簡介

在官網上對 OpenResty 是這樣介紹的(http://openresty.org):

“OpenResty 是一個基於 Nginx 與 Lua 的高效能 Web 平臺,其內部整合了大量精良的 Lua 庫、第三方模組以及大多數的依賴項。用於方便地搭建能夠處理超高併發、擴充套件性極高的動態 Web 應用、Web 服務和動態閘道器。”

“OpenResty 透過匯聚各種設計精良的 Nginx 模組(主要由 OpenResty 團隊自主開發),從而將 Nginx 有效地變成一個強大的通用 Web 應用平臺。這樣,Web 開發人員和系統工程師可以使用 Lua 指令碼語言調動 Nginx 支援的各種 C 以及 Lua 模組,快速構造出足以勝任 10K 乃至 1000K 以上單機併發連線的高效能 Web 應用系統。”

“OpenResty 的目標是讓你的 Web 服務直接跑在 Nginx 服務內部,充分利用 Nginx 的非阻塞 I/O 模型,不僅僅對 HTTP 客戶端請求,甚至於對遠端後端諸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都進行一致的高效能響應。”

從以上官網描述裡我們可以知道,OpenResty 官網對其定位是以 Nginx 為核心整合 Lua,打造一個兼具開發效率和高效能的服務端開發平臺。

OpenResty 的核心是基於 Nginx 的一個 C 模組(lua-Nginx-module),該模組將 LuaJIT 嵌入到 Nginx 伺服器中,並對外提供一套完整的 Lua API,透明地支援非阻塞 I/O,提供了輕量級執行緒、定時器等高階抽象。

我們可以用 Lua 語言來進行字串和數值運算、查詢資料庫、傳送 HTTP 請求、執行定時任務、呼叫外部命令等,還可以用 FFI 的方式呼叫外部 C 函式。這基本上可以滿足服務端開發需要的所有功能。

掌握好了 OpenResty,我們就可以同時擁有指令碼語言的開發效率和迭代速度,以及 Nginx C 模組的高併發和高效能優勢。

下面為大家介紹本文大綱

  • OpenResty 的 hello world 該怎麼寫
  • 快速上手 Lua 指令碼語言
  • OpenResty 用到的 Nginx 知識
  • OpenResty 在閘道器安全中如何應用

OpenResty 的 hello world 該怎麼寫

OpenResty 的安裝

OpenResty 的安裝有多種方法,比如使用作業系統的包管理器、原始碼編譯或者 docker 映象。推薦優先使用 yum、apt-get、brew 這類包管理系統,來安裝 OpenResty。

對於 Mac OS X 或 macOS 使用者,強烈推薦您使用 homebrew 包管理工具安裝 OpenResty。可以直接使用下面 這一條命令:

brew install openresty/brew/openresty

對於一些常見的 Linux 發行版本(Ubuntu、Debian、CentOS、RHEL、Fedora、OpenSUSE、Alpine 和 Amazon Linux), OpenResty 提供 官方預編譯包。確保首先用這種方式來安裝。這裡用 CentOS 舉例,可以使用如下方式,

CentOS 9 或者更新版本

# add the yum repo:
wget https://openresty.org/package/centos/openresty2.repo
sudo mv openresty2.repo /etc/yum.repos.d/openresty.repo

# update the yum index:
sudo yum check-update

CentOS 8 或者更老版本

# add the yum repo:
wget https://openresty.org/package/centos/openresty.repo
sudo mv openresty.repo /etc/yum.repos.d/openresty.repo

# update the yum index:
sudo yum check-update

然後就可以像下面這樣安裝軟體包,比如 openresty:

sudo yum install -y openresty

Docker 安裝

Docker 安裝的方式就最為簡單了,只需要輸入以下命令,就可以獲取打包好的映象。

docker pull openresty/openresty

目錄結構

安裝 OpenResty 成功後的目錄結構如下(以預設安裝目錄為例):

/usr/local/openresty/                          #安裝主目錄
├── bin                                     #存放可執行檔案
├── luajit                                  #LuaJIT執行庫
├── lualib                                  #Lua元件
├── Nginx                                   #Nginx核心執行平臺
├── pod                                     #參考手冊(restydoc)使用的資料
└── site                                    #包管理工具(opm)使用的資料

啟動服務

yum 安裝完後,就可以直接執行 openresty 命令,啟動 OpenResty 服務。

/usr/local/openresty/bin/openresty         #啟動OpenResty服務

OpenResty 預設開啟了 localhost:80 服務,使用 wget 或者 curl 這樣的工具就可以驗證 OpenResty 是否正常工作:

curl http://localhost:80                   #curl命令傳送HTTP請求

下面是一些其他常用命令,

/usr/local/openresty/bin/openresty  -s stop       #停止 OpenResty 服務
/usr/local/openresty/bin/openresty  -s reload     #重新載入 Nginx 配置檔案
/usr/local/openresty/bin/openresty  -t            #檢查 Nginx 配置檔案是否正確
/usr/local/openresty/bin/openresty  -c            #指定配置檔案啟動

OpenResty 的操作命令跟 Nginx 保持一致。可以執行 openresty -h 以及 nginx -h 對比看出,

每個後端都應該瞭解的OpenResty入門以及閘道器安全實戰

 

命令列工具 resty

如果你想安裝命令列工具 resty,那麼可以像下面這樣安裝 openresty-resty 包:

sudo yum install -y openresty-resty

resty 是一個 cli 工具,可以使用 -e 引數可以在命令列裡直接執行 Lua 程式碼,我們可以在命令列執行如下命令,

[root@VM-4-5-centos ~]# resty -e "print('hello world')"
hello OpenResty

resty 工具還有很多選項用於配置行為,非常靈活,-e 之外較常用的有

-c                :指定最大併發連線數(預設值是64);
-I                :指定Lua庫的搜尋路徑;
-l                :指定載入某個Lua庫;
--http-conf       :定製在http域裡的指令;
--main-include    :定製在main域裡的指令;
--shdict          :定製使用的共享記憶體(參見10.2節);
--resolve-ipv6    :允許解析ipv6的地址。

想了解完整的列表,可以檢視 resty -h 命令。

包管理工具 opm

跟大多數語言一樣有包管理工具一樣,OpenResty 也有自己的包管理工具 opm(OpenResty Package Manager),opm 在 openresty-opm 包裡,安裝命令如下,

sudo yum install -y openresty-opm

opm 是 OpenResty 自帶的包管理器,在你安裝好 OpenResty 之後,就可以直接使用。一些常見用法如下,

opm search    http                            #搜尋關鍵字http
opm search    kafka                           #搜尋關鍵字kafka
opm get       agentzh/lua-resty-http          #安裝元件,注意需要sudo
opm info      agentzh/lua-resty-http          #顯示元件的版本、作者等資訊
opm remove    agentzh/lua-resty-http          #移除元件,同樣需要sudo
opm --install-dir=/opt    get xxx             #把元件安裝到/opt目錄下
opm --cwd                 get xxx             #安裝到當前目錄的/resty_modules下

編寫 hello world

在上文中我們使用命令列工具 resty 寫了一個比較簡單的 OpenResty 程式,沒有 master 程式,也不會監聽埠。下面讓我們寫一個需要啟動 OpenResty 服務的 hello world。

首先找到 OpenResty 安裝目錄下 nginx/conf/nginx.conf 檔案,在 server 下新增 OpenResty 的 content_by_lua 指令,裡面嵌入了 ngx.say 的程式碼:

server {
        listen       88;
        server_name  localhost;

        location / {
            root   html;
            index  index.html index.htm;
        }

        location /hello {
            content_by_lua '
                ngx.say("hello, world")
            ';
        }
}

接著我們執行 openresty -s reload 命令,重新載入 nginx.conf 配置檔案。沒有報錯的話,OpenResty 的服務就已經成功啟動了。

最後使用 curl 命令,來檢視結果的返回:

[root@VM-4-5-centos conf]# curl localhost:88/hello
hello, world

到這裡,一個真正的 OpenResty 開發的 hello world 程式就完成了。

快速上手 Lua 指令碼語言

Lua 環境

我們不用專門去安裝標準 Lua 5.1 之類的環境,因為 OpenResty 已經不再支援標準 Lua,而只支援 LuaJIT。這裡我介紹的 Lua 語法,也是和 LuaJIT 相容的部分,而不是基於最新的 Lua 5.3,這一點需要特別注意。

在 OpenResty 的安裝目錄下,可以找到 LuaJIT 的目錄和可執行檔案。在 CentOS 系統下,LuaJIT 的目錄如下,

[root@VM-4-5-centos luajit]# cd /usr/local/openresty/luajit/bin/
[root@VM-4-5-centos bin]# ll
total 536
lrwxrwxrwx 1 root root     18 Oct 12 11:22 luajit -> luajit-2.1.0-beta3
-rwxr-xr-x 1 root root 547728 Jul 18 12:38 luajit-2.1.0-beta3

我們可以執行 cp luajit /usr/local/bin/ 將 luajit 檔案複製到 /usr/local/bin/ 目錄下,進而可以直接使用 luajit 命令。

檢視 LuaJIT 的版本號,

[root@VM-4-5-centos ~]# luajit  -v
LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2022 Mike Pall. https://luajit.org/

執行 lua 指令碼,

[root@VM-4-5-centos ~]# echo 'print("hello world")' > 1.lua
[root@VM-4-5-centos ~]# cat 1.lua
print("hello world")
[root@VM-4-5-centos ~]# luajit 1.lua
hello world
[root@VM-4-5-centos ~]#

也可以使用 resty 來直接執行,它最終也是用 LuaJIT 來執行的,

[root@VM-4-5-centos ~]# resty -e 'print("hello world")'
hello world

基本語法

變數

在 Lua 中宣告變數,可以如下程式碼所示,

local a = 'hello'
b = "world"

加了 local 關鍵字,用於宣告區域性變數。

不加 local 關鍵字的話,變數預設是全域性的。

註釋

兩個減號是單行註釋

-- 註釋

多行註釋

--[[
 多行註釋
 多行註釋
 --]]

行尾結束

Lua 中程式碼的行尾結束都不需要新增特殊字元,這跟 Java 不同(Java 在行尾需要新增 ;)。

local a = 'a'
print(a)

資料型別

Lua 中的資料型別不多,你可以透過 type 函式來返回一個值的型別,比如下面這樣的操作:

[root@VM-4-5-centos ~]# resty -e 'print(type("hello world"))
>  print(type(print))
>  print(type(true))
>  print(type(360.0))
>  print(type({}))
>  print(type(nil))
>  '

列印如下,

string
function
boolean
number
table
nil

這幾種就是 Lua 中的基本資料型別了。下面我們來簡單介紹一下它們。

字串

在 Lua 中,有三種方式可以表達一個字串:單引號、雙引號,以及長括號([[]]),示例如下,

新建 str.lua 檔案,寫入以下內容,

local s = 'a'
local s1 = "b"
local s2 = [[c]]

print(s)
print(s1)
print(s2)

執行 luajit str.lua 返回結果如下,

a
b
c

在 Lua 中,字串拼接採用 .. 的方式,示例如下,

編輯 str.lua 檔案,寫入以下內容,

local s = 'a'
local s1 = "b"
local s2 = [[c]]

print(s)
print(s1)
print(s2)

local s3 =s .. s1 ..s2
print(s3)

執行 luajit str.lua 返回結果如下,

a
b
c
abc

布林值

在 Lua 中,只有 nil 和 false 為假,其他都為 true,包括 0 和空字串也為真。我們可以用示例印證一下:

新建 bool.lua 指令碼檔案,寫入以下內容,

local a = 0
local b
if a then
  print("true")
end
a = ""
if a then
  print("true")
end

print(b)

執行 luajit str.lua 返回結果如下,

true
true
nil

在 Lua 中,空值就是 nil。如果你定義了一個變數,但沒有賦值,它的預設值就是 nil,對應的就是上面示例程式碼的區域性變數 b。

數字

Lua 的 number 型別,是用雙精度浮點數來實現的。值得一提的是,LuaJIT 支援 dual-number(雙數)模式,也就是說,LuaJIT 會根據上下文來用整型來儲存整數,而用雙精度浮點數來存放浮點數。示例如下,

新建 number.lua 指令碼檔案,寫入以下內容,

print(type(2))
print(type(2.2))
print(type(0.2))
print(type(2e+1))
print(type(0.2e-1))
print(type(7.8263692594256e-06))

print(2 + 2)
print(2 + 22.2)

執行 luajit number.lua 返回結果如下,

number
number
number
number
number
number
4
24.2

函式

函式在 Lua 中是一等公民,你可以把函式存放在一個變數中,也可以當作另外一個函式的入參和出參。示例如下,

新建 fun.lua 檔案,寫入以下程式碼,

-- 階乘
function factorial1(n)
    if n == 0 then
        return 1
    else
        return n * factorial1(n - 1)
    end
end
print(factorial1(5))
factorial2 = factorial1
print(factorial2(5))

執行 luajit fun.lua 返回結果如下,

120
120

分支控制

Lua 提供了以下兩種分支控制結構語句:

  • if 語句
  • if...else 語句
  • if...elseif...else 語句

if 語句

Lua if 語句語法格式如下:

if(布林表示式)
then
   --[ 在布林表示式為 true 時執行的語句 --]
end

以下是一個判斷變數 a 的值是否小於 20 的示例,

新建 if1.lua,寫入以下內容,

--[ 定義變數 --]
a = 10;

--[ 使用 if 語句 --]
if (a < 20) then
   --[ if 條件為 true 時列印以下資訊 --]
   print("a 小於 20" );
end
print("a 的值為:", a);

執行 luajit if1.lua 返回結果如下,

a 小於 20
a 的值為: 10

if...else 語句

Lua if 語句可以與 else 語句搭配使用, 在 if 條件表示式為 false 時執行 else 語句程式碼塊。

Lua if...else 語句語法格式如下:

if(布林表示式)
then
   --[ 布林表示式為 true 時執行該語句塊 --]
else
   --[ 布林表示式為 false 時執行該語句塊 --]
end

以下是一個判斷變數 a 值的示例,

新建 if2.lua,寫入以下內容,

--[ 定義變數 --]
a = 100;
--[ 檢查條件 --]
if( a < 20 )
then
   --[ if 條件為 true 時執行該語句塊 --]
   print("a 小於 20" )
else
   --[ if 條件為 false 時執行該語句塊 --]
   print("a 大於 20" )
end
print("a 的值為 :", a)

執行 luajit if2.lua 返回結果如下,

a 大於 20
a 的值為 : 100

if...elseif...else 語句

Lua if 語句可以與 elseif...else 語句搭配使用, 在 if 條件表示式為 false 時執行 elseif...else 語句程式碼塊,用於檢測多個條件語句。

Lua if...elseif...else 語句語法格式如下:

if( 布林表示式 1)
then
   --[ 在布林表示式 1 為 true 時執行該語句塊 --]

elseif( 布林表示式 2)
then
   --[ 在布林表示式 2 為 true 時執行該語句塊 --]

elseif( 布林表示式 3)
then
   --[ 在布林表示式 3 為 true 時執行該語句塊 --]
else
   --[ 如果以上布林表示式都不為 true 則執行該語句塊 --]
end

以下是一個判斷變數 a 值的示例,

新建 if3.lua,寫入以下內容,

--[ 定義變數 --]
a = 100

--[ 檢查布林條件 --]
if( a == 10 )
then
    --[ 如果條件為 true 列印以下資訊 --]
    print("a 的值為 10" )
elseif( a == 20 )
then
    --[ if else if 條件為 true 時列印以下資訊 --]
    print("a 的值為 20" )
elseif( a == 30 )
then
    --[ if else if condition 條件為 true 時列印以下資訊 --]
    print("a 的值為 30" )
else
    --[ 以上條件語句沒有一個為 true 時列印以下資訊 --]
    print("沒有匹配 a 的值" )
end
print("a 的真實值為: ", a )

執行 luajit if3.lua 返回結果如下,

沒有匹配 a 的值
a 的真實值為:  100

迴圈

Lua 程式語言中 for 迴圈語句可以重複執行指定語句,重複次數可在 for 語句中控制。

Lua 程式語言中 for 語句有兩大類:

  • 數值 for 迴圈
  • 泛型 for 迴圈

數值 for 迴圈

Lua 程式語言中數值 for 迴圈語法格式:

for var=exp1,exp2,exp3 do
    <執行體>
end

var 從 exp1 變化到 exp2,每次變化以 exp3 為步長遞增 var,並執行一次 "執行體"。exp3 是可選的,如果不指定,預設為 1。示例如下,

新建 for1.lua 檔案,寫入以下內容,

function f(x)
    print("function")
    return x*2
end

for i = 1, f(5) do print(i)
end

執行 luajit for1.lua 返回結果如下,

function
1
2
3
4
5
6
7
8
9
10

泛型 for 迴圈

泛型 for 迴圈透過一個迭代器函式來遍歷所有值,類似 java 中的 foreach 語句。

Lua 程式語言中泛型 for 迴圈語法格式:

--列印陣列a的所有值
local a = {"one", "two", "three"}
for i, v in ipairs(a) do
    print(i, v)
end

i 是陣列索引值,v 是對應索引的陣列元素值。ipairs 是 Lua 提供的一個迭代器函式,用來迭代陣列。

將以上內容下入 for2.lua 檔案,列印結果如下,

1 one
2 two
3 three

Lua 模組與包

模組類似於一個封裝庫,從 Lua 5.1 開始,Lua 加入了標準的模組管理機制,可以把一些公用的程式碼放在一個檔案裡,以 API 介面的形式在其他地方呼叫,有利於程式碼的重用和降低程式碼耦合度。

Lua 提供了一個名為 require 的函式用來載入模組。要載入一個模組,只需要簡單地呼叫就可以了。例如:

require("cjson")
-- 或者
require "cjson"

Lua 比較小巧,內建的標準庫並不多。在 OpenResty 的環境中預設支援了一些官方模組,如 cjson 可以直接使用,其他的一些第三方庫則需要先使用 lua_package_path 指令配置 OpenResty 的檔案定址路徑,又或者直接使用 opm 包管理工具來安裝一些第三方模組。

OpenResty 中預設啟用了下面列表的絕大部分元件,想要了解更多 OpenResty 相關元件的話,可以翻閱官網說明 https://openresty.org/cn/components.html。

LuaJIT
ArrayVarNginxModule
AuthRequestNginxModule
CoolkitNginxModule
DrizzleNginxModule
EchoNginxModule
EncryptedSessionNginxModule
FormInputNginxModule
HeadersMoreNginxModule
...

本文的 Lua 語法介紹到這裡就足夠在 OpenResty 中編寫 lua 指令碼了,想要了解更多 Lua 內容,如 table、檔案、調式等可以自行翻閱 https://www.runoob.com/lua/lua-tutorial.html 網站。

OpenResty 用到的 Nginx 知識

內建常量和變數

OpenResty 在內建 Lua 引擎中新增了一些常用的內建變數如下所示。

每個後端都應該瞭解的OpenResty入門以及閘道器安全實戰

 

圖片來源https://zhuanlan.zhihu.com/p/539546173

OpenResty 在內建 Lua 引擎中新增了一些常用的內建常量大致如下所示。

每個後端都應該瞭解的OpenResty入門以及閘道器安全實戰

 

每個後端都應該瞭解的OpenResty入門以及閘道器安全實戰

 

這些內建變數和常量都可以在 Lua 指令碼中直接使用。

配置指令

OpenResty 定義了一系列 Nginx 配置指令,用於配置何時執行使用者 Lua 指令碼以及如何返回 Lua 指令碼的執行結果,這些指令可以直接在 nginx.conf 配置檔案中使用。

OpenResty 定義的 Nginx 配置指令大致如下所示。

每個後端都應該瞭解的OpenResty入門以及閘道器安全實戰

 

圖片來源https://zhuanlan.zhihu.com/p/539546173

這些指令中有 9 個 *_by_lua 指令,它們和 Nginx 的關係如下圖所示

每個後端都應該瞭解的OpenResty入門以及閘道器安全實戰

 

圖片來自 lua-Nginx-module 文件

其中,init_by_lua 只會在 Master 程式被建立時執行,init_worker_by_lua 只會在每個 Worker 程式被建立時執行。其他的 *_by_lua 指令則是由終端請求觸發,會被反覆執行。

所以在 init_by_lua 階段,我們可以預先載入 Lua 模組和公共的只讀資料,這樣可以利用作業系統的 COW(copy on write)特性,來節省一些記憶體。

對於業務程式碼來說,其實大部分的操作都可以在 content_by_lua 裡面完成,但更推薦的做法,是根據不同的功能來進行拆分,比如下面這樣:

  • set_by_lua:設定變數;
  • rewrite_by_lua:轉發、重定向等;
  • access_by_lua:准入、許可權等;
  • content_by_lua:生成返回內容;
  • header_filter_by_lua:應答頭過濾處理;
  • body_filter_by_lua:應答體過濾處理;
  • log_by_lua:日誌記錄。

利用這些階段的特性,我們可以一些通用邏輯進行拆分處理,比如我們可以在 access 階段解密,在 body filter 階段加密就可以了,在 content 階段的程式碼是不用做任何修改的。

# 加密協議版本
location /test {
    access_by_lua '...';        # 請求體解密
    content_by_lua '...';       # 處理請求,不需要關心通訊協議
    body_filter_by_lua '...';   # 應答體加密
}

OpenResty 在閘道器安全中如何應用

WAF 介紹

Web 應用防火牆(Web Application Firewall,簡稱 WAF)對網站或者 App 的業務流量進行惡意特徵識別及防護,在對流量清洗和過濾後,將正常、安全的流量返回給伺服器,避免網站伺服器被惡意入侵導致效能異常等問題,從而保障網站的業務安全和資料安全。

常見 Web 應用攻擊防護

  • 防禦一些常見常見威脅:SQL 注入、XSS 跨站、WebShell 上傳、後門攻擊、命令注入、非法 HTTP 協議請求、常見 Web 伺服器漏洞攻擊、CSRF、核心檔案非授權訪問、路徑穿越、網站被掃描等。
  • CC 惡意攻擊防護:控制單一源 IP 的訪問頻率,基於重定向跳轉驗證、人機識別等。針對海量慢速請求攻擊,根據統計響應碼及 URL 請求分佈、異常 Referer 及 User-Agent 特徵識別,結合網站精準防護規則綜合防護。
  • 網站隱身:不對攻擊者暴露站點地址,避免其繞過 Web 應用防火牆直接攻擊。

相關產品

目前 WAF 相關產品主要有三類:

  • 硬體 WAF:效果好,但是貴!
  • 軟體 WAF:效果還算可以,能用,有開源產品!
  • 雲廠商 WAF:雲廠商的 WAF 都很貴!

鑑於極客精神(白嫖萬歲 ),這裡介紹幾款業內開源的 WAF 產品,

  • 長亭科技的雷池社群版,主頁地址:https://waf-ce.chaitin.cn/
  • ModSecurity,主頁地址:https://www.modsecurity.org/
  • Coraza,主頁地址:https://coraza.io/
  • VeryNginx,主頁地址:https://github.com/alexazhou/VeryNginx
  • NAXSI,主頁地址:https://github.com/nbs-system/naxsi
  • NGX_WAF,主頁地址:https://github.com/ADD-SP/ngx_waf
  • 南牆,主頁地址:https://waf.uusec.com/
  • JANUSEC,主頁地址:https://www.janusec.com/
  • HTTPWAF,主頁地址:https://github.com/httpwaf/httpwaf2.0
  • 錦衣盾,主頁地址:https://www.jxwaf.com/

對於以上 WAF 產品的一些評價指標如下:

  • 防護效果:主要是兩個維度,能不能防住攻擊,會不會影響普通使用者
  • 技術先進性:防護引擎的技術競爭力,是否具備對抗高階攻擊的能力
  • 專案質量:本文將以功能完整性、開原始碼質量、文件完整性等角度作為評價依據
  • 社群認可度:反映了專案在使用者社群中的聲譽和影響力,本文將以 GitHub Star 數作為評價依據
  • 社群活躍度:是潛力的體現,活躍度越高發展越快,本文將以社群使用者的參與度和作者維護專案的積極性作為

最終的的得分如下,

每個後端都應該瞭解的OpenResty入門以及閘道器安全實戰

 

圖片來源https://stack.chaitin.com/techblog/detail/115

需要注意的是軟體 WAF 一般在第 7 層中進行防禦(osi 模型),並非能夠防禦所有型別的攻擊,比如 ddos 攻擊就不能防禦。不過一般雲廠商提供的 WAF 產品也有攜帶了 ddos 攻擊防禦的支援,比如阿里雲。

OpenResty 在 WAF 中的應用

使用 OpenResty 作為流量入口時,我們可以透過編寫一些 Lua 指令碼來實現 WAF 防禦的功能。Lua 指令碼可以在 Nginx 配置檔案中指定,在不同的階段執行。

對於防火牆功能,我們通常可以在 access_by_lua 階段執行 Lua 指令碼,用於匹配請求或響應的頭部或內容,並根據匹配結果決定是否放行資料包或返回錯誤資訊。

下面我將給大家演示如何使用 OpenResty 實現一個基於 Lua 的 WAF(Web Application Firewall)功能。用來識別和阻止常見的 Web 攻擊,如 cc 防禦、ip 黑名單、ua 引數校驗等。

cc 防禦

  1. 修改 nginx.conf 檔案,加入 access_by_lua_file cc.lua 指令,
http {
  # 宣告一個 10m 大小的共享記憶體 cc_dict
  lua_shared_dict cc_dict 10m;
  lua_package_path "/usr/local/openresty/nginx/conf/lua/waf/?.lua;/usr/local/openresty/lualib/?.lua;";
  ...
  server {
    listen       88;
    server_name  localhost;

    # 在access階段執行 cc 防禦外掛
    access_by_lua_file cc.lua;

    location / {
      ...
    }
  }
}
  1. 新建 cc.lua 指令碼,寫入以下內容,
-- 獲取客戶端ip
local function getClientIp()
    IP  = ngx.var.remote_addr
    if IP == nil then
        IP  = "unknown"
    end
    return IP
end

local function denyCC()
    local uri=ngx.var.uri
    ccCount=100
    ccSeconds=6
    local access_uri = getClientIp()..uri
    local limit = ngx.shared.cc_dict
    local req,_=limit:get(access_uri)
    if req then
        if req > ccCount then
            ngx.exit(503)
            return true
        else
            limit:incr(access_uri,1)
        end
    else
        limit:set(access_uri,1,ccSeconds)
    end
    return false
end

if denyCC() then
    return
end
  1. 重啟 OpenResty 服務,就完成了 cc 防禦功能。
openresty -s  reload

ip 黑名單

  1. 修改 nginx.conf 檔案,加入 access_by_lua_file ip_block.lua 指令,
http {
  lua_package_path "/usr/local/openresty/nginx/conf/lua/waf/?.lua;/usr/local/openresty/lualib/?.lua;";
  ...
  server {
    listen       88;
    server_name  localhost;

    # 在access階段執行 ip_block 防禦外掛
    access_by_lua_file ip_block.lua;

    location / {
      ...
    }
  }
}
  1. 新建 ip_block.lua 指令碼,寫入以下內容,
local cjson = require "cjson"

local function read_json(var)
    file = io.open(var,"r")
    if file==nil then
        return
    end
    str = file:read("*a")
    file:close()
    list = cjson.decode(str)
    return list
end

local function getClientIp()
    IP  = ngx.var.remote_addr
    if IP == nil then
        IP  = "unknown"
    end
    return IP
end

local function blockIpCheck()
    local ipBlockList=read_json('/usr/local/openresty/nginx/conf/lua/waf/ip_block.json')
    if next(ipBlockList) ~= nil then
        for _,ip in pairs(ipBlockList) do
            if getClientIp()==ip then
                ngx.exit(403)
                return true
            end
        end
    end
    return false
end

if blockIpCheck() then
    return
end
  1. 在 /usr/local/openresty/nginx/conf/lua/waf 目錄下新建 ip_block.json 檔案,寫入我們要加入黑名單的 ip,
["58.48.224.7"]
  1. 重啟 OpenResty 服務,就完成了 ip 黑名單功能。
openresty -s  reload

ua 攔截

  1. 修改 nginx.conf 檔案,加入 access_by_lua_file ua.lua 指令,
http {
  lua_package_path "/usr/local/openresty/nginx/conf/lua/waf/?.lua;/usr/local/openresty/lualib/?.lua;";
  ...
  server {
    listen       88;
    server_name  localhost;

    # 在access階段執行 ua 防禦外掛
    access_by_lua_file ua.lua;

    location / {
      ...
    }
  }
}
  1. 新建 ua.lua 指令碼,寫入以下內容,
local ngxMatch=ngx.re.match
local cjson = require "cjson"

local function read_json(var)
    file = io.open(var,"r")
    if file==nil then
        return
    end
    str = file:read("*a")
    file:close()
    list = cjson.decode(str)
    return list
end

function ua()
    local ua = ngx.var.http_user_agent
    local userAgents=read_json('/usr/local/openresty/nginx/conf/lua/waf/user_agent.json')
    if next(userAgents) ~= nil then
        for _,rule in pairs(userAgents) do
            if rule ~="" and ngxMatch(ua,rule,"isjo") then
                ngx.exit(403)
                return true
            end
        end
    end
    return false
end

if ua() then
    return
end
  1. 在 /usr/local/openresty/nginx/conf/lua/waf 目錄下新建 user_agent.json 檔案,寫入我們要加入黑名單的 ua 資訊,
["Chrome/116.0.0.0"]
  1. 重啟 OpenResty 服務,就完成了 ua 攔截功能。
openresty -s  reload

相關資料

  • OpenResty 官網:https://openresty.org/cn/benchmark.html
  • 菜鳥教程:https://www.runoob.com/lua/lua-tutorial.html
  • 《OpenResty完全開發指南》:https://weread.qq.com/web/bookDetail/fec3240071848696fec3572
  • 《OpenResty從入門到實戰》:https://time.geekbang.org/column/intro/186?code=hkx6qkdp47iccvn0yf40aowqzyzzchyykmswfogb90g%3D

總結

自此本文介紹了OpenResty入門以及使用 Lua 指令碼實現一些常見的閘道器安全功能等。需要注意的就是大家在已有的 Nginx 服務遷移到 OpenResty 上來時,記得注意 OpenResty 版本,Nginx 與 OpenResty 相同版本情況下,OpenResty 官方是保證完全相容的。

最後感謝大家閱讀,希望本文能對你有所幫助。

關注公眾號【waynblog】每週分享技術乾貨、開源專案、實戰經驗、國外優質文章翻譯等,您的關注將是我的更新動力!

相關文章