WAF 自定義動作
WAF 自定義動作在請求滿足配置的 WAF 動作觸發條件時呼叫全域性 Lua 模組。模組接收 WAF 判定結果,可以返回響應、展示自己的挑戰頁面,或者繼續處理請求。
配置步驟
建立一個全域性 Lua 模組,按下文約定匯出相應回撥,其中只有
invoke是必需的。
在應用的頁面規則 WAF 配置中,將攔截動作設為自定義動作,再在模組名中選擇該模組。
配置 WAF 規則、偏執級別和敏感級別,然後儲存併發布頁面規則變更。

請求已有有效的 Edge 挑戰驗證放行憑證時,會跳過此動作。
invoke 回撥
Edge 以普通函式形式呼叫 invoke(params),不傳入隱含的 self 引數。params 是 Lua table,不是 JSON 字串,也沒有額外的請求上下文引數。
| 欄位 | Lua 型別 | 說明 |
|---|---|---|
time | number | 挑戰建立時間,Unix 時間戳,精度為整秒。 |
module_name | string | 選定的全域性 Lua 模組名稱。 |
clearance_time | number 或 nil | 驗證成功後的放行時長,單位為秒。管理後臺的自定義動作沒有該配置項,因此只有通過管理 API 配置規則時才有值,否則為 nil。 |
captcha_type | string | 固定為 "custom"。 |
token | string | 已加密、Base64 編碼並經過 URI 轉義的 token,用於後續挑戰請求。不要自行解析其內容。 |
prev_url | string | 原始請求 URI,包含查詢引數,已經過 URI 轉義。 |
verdict | table | WAF 判定結果,見下文。 |
WAF 判定結果
| 欄位 | Lua 型別 | 說明 |
|---|---|---|
score | number | 此次動作判定使用的分數;開啟跨請求模式時為累計分數。顯式 WAF 判定可能使用特殊分值,不應假設分數始終為正數。 |
action | string | 固定為 "custom-action",即攔截動作選擇自定義動作時 Edge 使用的取值。 |
threshold | number 或 nil | 觸發該動作的分數閾值,由敏感級別或最低分數設定;沒有閾值時可預設。 |
hit_types | 字串陣列 | 命中的規則分組,不應依賴其排列順序。 |
rule_sets | table 或 nil | 規則集 ID 到 "分數/閾值" 字串的對映,僅包含達到各自閾值的規則集。它不是所有命中規則集的列表,可能預設,開啟跨請求模式時也可能沒有此欄位。 |
rules | table 陣列 | 命中的規則;每項包含 rule_name(字串,未設定時可預設)、rule_id(數值)和 group(字串)。 |
例如,rule_sets 中的 [12] = "10/5" 表示規則集 12 的分數為 10,閾值為 5。rules 不包含單條規則的分數、命中的請求資料或完整 WAF 日誌。請將判定結果視為只讀資料,並處理可選欄位預設的情況。
返回值
| 返回值 | 行為 |
|---|---|
content, status | 以數值型 HTTP 狀態碼返回響應體並結束請求,預設內容型別為 text/html。 |
content | 以 HTTP 403 返回響應體並結束請求。 |
nil, "pass" | 繼續處理請求,不傳送響應,也不簽發放行 cookie。其他規則仍可能影響此請求。 |
nil 或 nil, 429 | 因為沒有提供響應體,返回 HTTP 403。 |
需要空響應體時,返回空字串,例如 return "", 429。應將響應體和狀態碼返回給 Edge,不要先輸出響應體再返回。找不到模組或模組沒有匯出 invoke 時,返回 HTTP 500。
示例:返回 HTTP 429
以下模組返回靜態攔截頁面,不實現挑戰驗證,也不簽發放行憑證:
local _M = {}
local function invoke(params)
return [[<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Request temporarily blocked</title></head>
<body><h1>Request temporarily blocked</h1><p>Please try again later.</p></body>
</html>]], 429
end
_M.invoke = invoke
return _M
自定義挑戰頁面同樣可以返回挑戰 HTML 和 429。這樣會在生成響應時直接設定狀態碼,無需在響應頭過濾階段將 403 改成 429。此返回值僅影響本次自定義響應,不會改變內建攔截動作、內建挑戰或挑戰驗證失敗的狀態碼。
如果模組自行檢查後決定放行當前請求,返回 nil, "pass"。這不會為後續請求授予放行憑證。
挑戰回撥
僅攔截或放行請求的模組只需匯出 invoke。要實現完整的挑戰流程——返回挑戰頁面、校驗答案,並在一段時間內放行該客戶端——還需匯出 verify;如果挑戰頁面需要單獨獲取挑戰內容,再匯出 create。
Edge 將兩個固定入口路由到這兩個回撥。它們都通過 invoke 收到的 token 識別模組,因此挑戰頁面必須把該 token 傳遞下去。
這兩個回撥的 params 都從 token 解碼得到,包含 time、module_name、clearance_time、captcha_type 和 verdict,不包含 Edge 為 invoke 額外新增的 token 和 prev_url。傳給 invoke 的 token 和 prev_url 已經過 URI 轉義,拼入下文的查詢引數或表單請求體時不要再次編碼。
create
GET /.edge-waf/create-captcha?token=TOKEN
Edge 呼叫 create(params, uri_args):
| 引數 | Lua 型別 | 說明 |
|---|---|---|
params | table | 從 token 解碼得到的資料,見上文。 |
uri_args | table | 解析後的查詢引數,包含 token。使用其他客戶端傳入的值前應先校驗。 |
| 返回值 | 行為 |
|---|---|
| 非空字串 | 作為響應體以 HTTP 200 返回。 |
nil 或 "" | 返回 HTTP 403。 |
第二個返回值會被忽略,狀態碼只會是 200 或 403。Edge 不會為該響應設定 Content-Type,如果客戶端需要,請在回撥中用 ngx.header.content_type 設定。
請求該入口時如果缺少 token 或 token 無法解碼,請求不會到達模組,而是回退到 Edge 內建的驗證碼圖片。token 能夠解碼、但模組沒有匯出 create 時,返回 HTTP 500。
verify
POST /.edge-waf/edge-recaptcha
Content-Type: application/x-www-form-urlencoded
請求體必須原樣攜帶 invoke 中的 token 和 prev_url,以及自定義的答案欄位。缺少其中任一個時,Edge 返回 HTTP 403。
Edge 呼叫 verify(params, post_args):
| 引數 | Lua 型別 | 說明 |
|---|---|---|
params | table | 從 token 解碼得到的資料,見上文。 |
post_args | table | 解析後的表單欄位。信任客戶端提交的答案前應先校驗。 |
| 返回值 | 行為 |
|---|---|
true | Edge 簽發放行憑證,並以 HTTP 200 返回 URI 反轉義後的 prev_url 作為響應體,由頁面據此跳轉。 |
false 或 nil | 返回 HTTP 403。 |
放行憑證是有效期為 params.clearance_time 秒的 waf-verify cookie;未配置放行時長時為 60 秒,管理後臺配置的規則即屬於這種情況。該 cookie 有效期內會完全跳過此 WAF 動作,因此不會再次呼叫 invoke。模組沒有匯出 verify 時,返回 HTTP 500。
示例:完整的挑戰流程
invoke 返回攜帶 token 的頁面,頁面向 create 獲取題目,verify 校驗答案並簽發放行憑證:
local _M = {}
local str_fmt = string.format
local PAGE = [[<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Checking your request</title></head>
<body>
<p id="question">Loading...</p>
<input id="answer"><button onclick="send()">Continue</button>
<script>
var token = "%s", prevUrl = "%s";
fetch("/.edge-waf/create-captcha?token=" + token)
.then(function (r) { return r.json(); })
.then(function (c) { document.getElementById("question").textContent = c.question; });
function send() {
fetch("/.edge-waf/edge-recaptcha", {
method: "POST",
headers: {"Content-Type": "application/x-www-form-urlencoded"},
body: "token=" + token + "&prev_url=" + prevUrl
+ "&answer=" + encodeURIComponent(document.getElementById("answer").value)
}).then(function (r) {
if (r.status === 200) {
r.text().then(function (url) { location.replace(url); });
}
});
}
</script>
</body>
</html>]]
local function invoke(params)
return str_fmt(PAGE, params.token, params.prev_url), 429
end
_M.invoke = invoke
local function create(params, uri_args)
ngx.header.content_type = "application/json"
return [[{"question": "What is two plus three?"}]]
end
_M.create = create
local function verify(params, post_args)
if post_args.answer ~= "5" then
return false
end
return true
end
_M.verify = verify
return _M
模組也可以不使用 Edge 的入口,而是自建挑戰入口:在頁面規則中用 foreign-call() 把自定義 URI 路由到 create 和 verify,自行管理放行 cookie,並在該 cookie 存在時讓 invoke 返回 nil, "pass"。這樣呼叫時,回撥只會收到規則傳入的引數,而不是 Edge 的 params,需要自行讀取請求內容。
在 Edgelang 挑戰介面中使用這三個回撥的說明和驗證碼實現示例,參見自定義挑戰。