對(duì)用戶輸入的數(shù)據(jù)進(jìn)行合法性檢查,避免錯(cuò)誤非法的數(shù)據(jù)進(jìn)入服務(wù),這是業(yè)務(wù)系統(tǒng)最常見的需求。很可惜 Lua 目前沒有特別好的數(shù)據(jù)合法性檢查庫(kù)。
坦誠(chéng)我們自己做的也不夠好,這里只能拋磚引玉,看看大家是否有更好辦法。
我們有這么幾個(gè)主要的合法性檢查場(chǎng)景:
這里主要是 json decode 時(shí),可能拋出異常的問題。我們已經(jīng)在 json 解析的異常捕獲 一章中詳細(xì)說明了問題本身以及解決方法,這里就不再重復(fù)。
HEX 編碼,最常見的存在有 MD5 值等。他們是由 0-9,A-F(或 a-f)組成。筆者把使用過的代碼版本逐一羅列,并進(jìn)行性能測(cè)試。通過這個(gè)測(cè)試,我們不僅僅可以收獲參數(shù)校驗(yàn)的正確寫法,以及可以再次印證一下效率最高的匹配,應(yīng)該注意什么。
require "resty.core.regex"
-- 純 lua 版本,優(yōu)點(diǎn)是兼容性好,可以適用任何 lua 語(yǔ)言環(huán)境
function check_hex_lua( str )
if "string" ~= type(str) then
return false
end
for i = 1, #str do
local ord = str:byte(i)
if not (
(48 <= ord and ord <= 57) or
(65 <= ord and ord <= 70) or
(97 <= ord and ord <= 102)
) then
return false
end
end
return true
end
-- 使用 ngx.re.* 完成,沒有使用任何調(diào)優(yōu)參數(shù)
function check_hex_default( str )
if "string" ~= type(str) then
return false
end
return ngx.re.find(str, "[^0-9a-fA-F]") == nil
end
-- 使用 ngx.re.* 完成,使用調(diào)優(yōu)參數(shù) "jo"
function check_hex_jo( str )
if "string" ~= type(str) then
return false
end
return ngx.re.find(str, "[^0-9a-fA-F]", "jo") == nil
end
-- 下面就是測(cè)試用例部分代碼
function do_test( name, fun )
ngx.update_time()
local start = ngx.now()
local t = "012345678901234567890123456789abcdefABCDEF"
assert(fun(t))
for i=1,10000*300 do
fun(t)
end
ngx.update_time()
print(name, "\ttimes:", ngx.now() - start)
end
do_test("check_hex_lua", check_hex_lua)
do_test("check_hex_default", check_hex_default)
do_test("check_hex_jo", check_hex_jo)
把上面的源碼在 OpenResty 環(huán)境中運(yùn)行,輸出結(jié)果如下:
? resty test.lua
check_hex_lua times:1.0390000343323
check_hex_default times:5.1929998397827
check_hex_jo times:0.4539999961853
不知道這個(gè)結(jié)果大家是否有些意外,check_hex_default
的運(yùn)行效率居然比 check_hex_lua
要差。不過所幸的是我們對(duì)正則開啟了 jo
參數(shù)優(yōu)化后,速度上有明顯提升。
引用一下 ngx.re.* 官方 wiki 的原文:在優(yōu)化性能時(shí),o
選項(xiàng)非常有用,因?yàn)檎齽t表達(dá)式模板將僅僅被編譯一次,之后緩存在 worker 級(jí)的緩存中,并被此 Nginx worker 處理的所有請(qǐng)求共享。緩存數(shù)量上限可以通過 lua_regex_cache_max_entries 指令調(diào)整。
課后小作業(yè):為什么測(cè)試用例中要使用 ngx.update_time() 呢?好好想一想。 課后小作業(yè):在測(cè)試用例里面加了一行
require "resty.core.regex"
。試試去掉這一行,重新跑下程序。結(jié)果怎么樣?
當(dāng)我們接收客戶端請(qǐng)求,除了指定字段的特殊校驗(yàn)外,我們最常見的需求就是對(duì)指定字段的類型做限制了。比如用戶注冊(cè)接口,我們就要求對(duì)方姓名、郵箱等是個(gè)字符串,手機(jī)號(hào)、電話號(hào)碼等是個(gè)數(shù)字類型,詳細(xì)信息可能是個(gè)圖片又或者是個(gè)嵌套的 TABLE。
例如我們接受用戶的注冊(cè)請(qǐng)求,注冊(cè)接口示例請(qǐng)求 body 如下:
{
"username":"myname",
"age":8,
"tel":88888888,
"mobile_no":13888888888,
"email":"***@**.com",
"love_things":["football", "music"]
}
這時(shí)候可以用一個(gè)簡(jiǎn)單的字段描述格式來表達(dá)限制關(guān)系,如下:
{
"username":"",
"age":0,
"tel":0,
"mobile_no":0,
"email":"",
"love_things":[]
}
對(duì)于有效字段描述格式,數(shù)據(jù)值是不敏感的,但是數(shù)據(jù)類型是敏感的,只要數(shù)據(jù)類型能匹配,就可以讓我們輕松不少。
來看下面的參數(shù)校驗(yàn)代碼以及基本的測(cè)試用例:
function check_args_template(args, template)
if type(args) ~= type(template) then
return false
elseif "table" ~= type(args) then
return true
end
for k,v in pairs(template) do
if type(v) ~= type(args[k]) then
return false
elseif "table" == type(v) then
if not check_args_template(args[k], v) then
return false
end
end
end
return true
end
local args = {name="myname", tel=888888, age=18,
mobile_no=13888888888, love_things = {"football", "music"}}
print("valid check: ", check_args_template(args, {name="", tel=0, love_things={}}))
print("unvalid check: ", check_args_template(args, {name="", tel=0, love_things={}, email=""}))
運(yùn)行一下上面的代碼,結(jié)果如下:
? resty test.lua
valid check: true
unvalid check: false
可以看到,當(dāng)我們業(yè)務(wù)層面需要有 email 地址但是請(qǐng)求方?jīng)]有上送,這時(shí)候就能檢測(cè)出來了。大家看到這里也許會(huì)笑,尤其是從其他成熟 web 框架中過來的同學(xué),我們這里的校驗(yàn)可以說是比較粗糙簡(jiǎn)陋的,很多開源框架中的參數(shù)限制,都可以做到更加精確的限制。
如果你有更好更優(yōu)雅的解決辦法,歡迎與我們聯(lián)系。