validation: Lua 和 nginx-module-lua 的验证库(输入验证和过滤)
安装
如果您尚未设置 RPM 仓库订阅,请 注册。然后您可以继续执行以下步骤。
CentOS/RHEL 7 或 Amazon Linux 2
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-validation
CentOS/RHEL 8+、Fedora Linux、Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-validation
要在 NGINX 中使用此 Lua 库,请确保已安装 nginx-module-lua。
本文档描述了 lua-resty-validation v2.7,于 2017 年 8 月 25 日发布。
lua-resty-validation 是一个可扩展的链式验证和过滤库,适用于 Lua 和 OpenResty。
使用 lua-resty-validation 的 Hello World
local validation = require "resty.validation"
local valid, e = validation.number:between(0, 9)(5) -- valid = true, e = 5
local valid, e = validation.number:between(0, 9)(50) -- valid = false, e = "between"
-- 验证器可以重用
local smallnumber = validation.number:between(0, 9)
local valid, e = smallnumber(5) -- valid = true, e = 5
local valid, e = smallnumber(50) -- valid = false, e = "between"
-- 验证器可以进行过滤(即修改被验证的值)
-- valid = true, s = "HELLO WORLD!"
local valid, s = validation.string.upper "hello world!"
-- 您可以用自己的验证器和过滤器扩展验证库...
validation.validators.capitalize = function(value)
return true, value:gsub("^%l", string.upper)
end
-- ... 然后使用它
local valid, e = validation.capitalize "abc" -- valid = true, e = "Abc"
-- 您还可以对多个值进行分组验证
local group = validation.new{
artist = validation.string:minlen(5),
number = validation.tonumber:equal(10)
}
local valid, fields, errors = group{ artist = "Eddie Vedder", number = "10" }
if valid then
print("所有组字段都是有效的")
else
print(fields.artist.name, fields.artist.error,
fields.artist.valid, fields.artist.invalid,
fields.artist.input, fields.artist.value,
fields.artist.validated, fields.artist.unvalidated)
end
-- 您甚至可以调用 fields 来获取简单的名称、值表
-- (在这种情况下,所有的 `nil` 也会被移除)
-- 默认情况下,这只返回有效字段的名称和值:
local data = fields()
local data = fields "valid"
-- 要仅获取无效字段的名称和值,请调用:
local data = fields "invalid"
-- 要仅获取已验证字段的名称和值,请调用(无论它们是否有效):
local data = fields "validated"
-- 要仅获取未验证字段的名称和值,请调用(无论它们是否有效):
local data = fields "unvalidated"
-- 要获取所有字段,请调用:
local data = fields "all"
-- 或者组合:
local data = fields("valid", "invalid")
-- 这并不止于此。您可能还想通过名称仅获取某些字段。
-- 您可以通过调用(返回一个表)来做到这一点:
local data = data{ "artist" }
内置验证器和过滤器
lua-resty-validation 附带了几个内置验证器,项目欢迎更多验证器的贡献。
无参数的验证器和过滤器
类型验证器可用于验证被验证值的类型。这些验证器是无参数的验证器(用点 . 调用它们):
null或["nil"](因为 nil 是 Lua 中的保留关键字)booleannumberstringtableuserdatafunc或["function"](因为 function 是 Lua 中的保留关键字)callable(可以是一个函数或一个具有元方法__call的表)threadintegerfloatfile(io.type(value) == 'file')
类型转换过滤器:
tostringtonumbertointegertoboolean
其他过滤器:
tonil或tonullabsinfnanfinitepositivenegativeloweruppertrimltrimrtrimreverseemailoptional
示例
local validation = require "resty.validation"
local ok, e = validation.null(nil)
local ok, e = validation.boolean(true)
local ok, e = validation.number(5.2)
local ok, e = validation.string('Hello, World!')
local ok, e = validation.integer(10)
local ok, e = validation.float(math.pi)
local f = assert(io.open('filename.txt', "r"))
local ok, e = validation.file(f)
验证工厂验证器和过滤器
验证工厂由不同的验证器和过滤器组成,用于验证或过滤值(用冒号 : 调用它们):
type(t),验证值的类型为t(见类型验证器)nil()或["null"](),检查值类型是否为nilboolean(),检查值类型是否为booleannumber(),检查值类型是否为numberstring(),检查值类型是否为stringtable(),检查值类型是否为tableuserdata(),检查值类型是否为userdatafunc()或["function"](),检查值类型是否为functioncallable(),检查值是否可调用(即函数或具有元方法__call的表)thread(),检查值类型是否为threadinteger(),检查值类型是否为integerfloat(),检查值类型是否为floatfile(),检查值类型是否为file(io.type(value) == 'file')abs(),过滤值并返回绝对值(math.abs)inf(),检查值是否为inf或-infnan(),检查值是否为nanfinite(),检查值是否不是nan、inf或-infpositive(),验证值是否为正(> 0)negative(),验证值是否为负(< 0)min(min),验证值至少为min(>=)max(max),验证值至多为max(<=)between(min[, max = min]),验证值在min和max之间outside(min[, max = min]),验证值不在min和max之间divisible(number),验证值是否能被number整除indivisible(number),验证值是否不能被number整除len(min[, max = min]),验证值的长度是否恰好为min或在min和max之间(UTF-8)minlen(min),验证值的长度是否至少为min(UTF-8)maxlen(max),验证值的长度是否至多为max(UTF-8)equals(equal)或equal(equal),验证值是否恰好等于某个值unequals(equal)或unequal(equal),验证值是否不等于某个值oneof(...),验证值是否等于提供的参数之一noneof(...),验证值是否不等于提供的任何参数match(pattern[, init]),验证值是否匹配(string.match)模式unmatch(pattern[, init]),验证值是否不匹配(string.match)模式tostring(),将值转换为字符串tonumber([base]),将值转换为数字tointeger(),将值转换为整数toboolean(),将值转换为布尔值(使用not not value)tonil()或tonull(),将值转换为 nillower(),将值转换为小写(尚未实现 UTF-8 支持)upper(),将值转换为大写(尚未实现 UTF-8 支持)trim([pattern]),修剪左侧和右侧的空白(您也可以使用模式)ltrim([pattern]),修剪左侧的空白(您也可以使用模式)rtrim([pattern]),修剪右侧的空白(您也可以使用模式)starts(starts),检查字符串是否以starts开头ends(ends),检查字符串是否以ends结尾reverse,反转值(字符串或数字)(UTF-8)coalesce(...),如果值为 nil,则返回作为参数传递的第一个非 nil 值email(),验证值是否为电子邮件地址call(function),根据自定义内联验证器/过滤器验证/过滤值optional([default]),如果值为空字符串""或nil,则停止验证并返回true,并返回default或value
条件验证工厂验证器
对于所有验证工厂验证器,都有一个条件版本,始终验证为 true,但您可以根据原始验证器的验证情况替换实际值。嘿,这比说出来更容易:
local validation = require "resty.validation"
-- ok == true, value == "Yes, the value is nil"
local ok, value = validation:ifnil(
"Yes, the value is nil",
"No, you did not supply a nil value")(nil)
-- ok == true, value == "No, you did not supply a nil value"
local ok, value = validation:ifnil(
"Yes, the value is nil",
"No, you did not supply a nil value")("non nil")
-- ok == true, value == "Yes, the number is between 1 and 10"
local ok, value = validation:ifbetween(1, 10,
"Yes, the number is between 1 and 10",
"No, the number is not between 1 and 10")(5)
-- ok == true, value == "No, the number is not between 1 and 10"
local ok, value = validation:ifbetween(1, 10,
"Yes, the number is between 1 and 10",
"No, the number is not between 1 and 10")(100)
条件验证工厂验证器的最后两个参数是 truthy 和 falsy 值。其他每个参数都传递给实际的验证工厂验证器。
组验证器
lua-resty-validation 目前支持一些预定义的验证器:
compare(comparison),比较两个字段并根据比较结果设置字段为无效或有效requisite{ fields },至少需要一个必要字段,即使它们本身是可选的requisites({ fields }, number),至少需要number个必要字段(默认情况下是所有字段)call(function),调用自定义(或内联)组验证函数
local ispassword = validation.trim:minlen(8)
local group = validation.new{
password1 = ispassword,
password2 = ispassword
}
group:compare "password1 == password2"
local valid, fields, errors = group{ password1 = "qwerty123", password2 = "qwerty123" }
local optional = validation:optional"".trim
local group = validation.new{
text = optional,
html = optional
}
group:requisite{ "text", "html" }
local valid, fields, errors = group{ text = "", html = "" }
local optional = validation:optional ""
local group = validation.new{
text = optional,
html = optional
}
group:requisites({ "text", "html" }, 2)
-- 或 group:requisites{ "text", "html" }
local valid, fields, errors = group{ text = "", html = "" }
group:call(function(fields)
if fields.text.value == "hello" then
fields.text:reject "text cannot be 'hello'"
fields.html:reject "because text was 'hello', this field is also invalidated"
end
end)
您可以在 compare 组验证器中使用普通 Lua 关系运算符:
<><=>===~=
requisite 和 requisites 检查字段值是否为 nil 或 ""(空字符串)。使用 requisite,如果所有指定字段都是 nil 或 "",则所有字段都是无效的(前提是它们本身不是无效的),如果至少有一个字段有效,则所有字段都是有效的。requisites 的工作方式相同,但您可以定义希望有多少字段的值不是 nil 且不是空字符串 ""。这些提供了条件验证的意义:
- 我有(两个或更多)字段
- 所有字段都是可选的
- 至少一个/定义数量的字段应该被填充,但我不在乎哪个,只要至少有一个/定义数量的字段被填充
停止验证器
停止验证器,如 optional,与普通验证器类似,但它们不是返回 true 或 false 作为验证结果或过滤值,而是可以返回 validation.stop。此值也可以在条件验证器和支持默认值的验证器中使用。以下是 optional 验证器的实现:
function factory.optional(default)
return function(value)
if value == nil or value == "" then
return validation.stop, default ~= nil and default or value
end
return true, value
end
end
这些大致相等:
-- 两者都返回: true, "default"(它们在 nil 和 "" 输入上停止处理 :minlen(10))
local input = ""
local ok, val = validation.optional:minlen(10)(input)
local ok, val = validation:optional(input):minlen(10)(input)
local ok, val = validation:ifoneof("", nil, validation.stop(input), input):minlen(10)(input)
过滤值并将值设置为 nil
大多数不过滤值的验证器仅返回 true 或 false 作为结果。这意味着现在没有办法向 resty.validation 发出信号,实际将值设置为 nil。因此,有一个变通方法,您可以返回 validation.nothing 作为值,这将把值更改为 nil,例如,内置的 tonil 验证器实际上是这样实现的(伪代码):
function()
return true, validation.nothing
end
自定义(内联)验证器和过滤器
有时您可能只有一次性的验证器/过滤器,您不会在其他地方使用,或者您只是想快速提供一个额外的验证器/过滤器以应对特定情况。为了使这一切变得简单明了,我们在 lua-resty-validation 2.4 中引入了 call 工厂方法。以下是一个示例:
validation:call(function(value)
-- 现在验证/过滤值,并返回结果
-- 在这里我们只是返回 false(即使验证失败)
return false
end)("Check this value"))
(当然,它不需要是内联函数,因为在 Lua 中,所有函数都是一等公民,可以作为参数传递)
内置验证器扩展
目前 lua-resty-validation 支持两个扩展或插件,您可以启用它们:
resty.validation.ngxresty.validation.tzresty.validation.utf8
如果您想构建自己的验证器扩展,可以查看这些。如果您这样做,并认为它对其他人也有用,请务必将您的扩展作为拉取请求发送以纳入此项目,非常感谢您,;-).
resty.validation.ngx 扩展
顾名思义,这组验证器扩展需要 OpenResty(或至少需要 Lua Nginx 模块)。要使用此扩展,您只需:
require "resty.validation.ngx"
它将猴子补丁提供的适配器到 resty.validation 中,目前包括:
escapeuriunescapeuribase64encbase64deccrc32shortcrc32longcrc32md5
(这些都有工厂和无参数版本)
ngx 扩展中还有正则表达式匹配器,使用 ngx.re.match,以及参数化的 md5:
regex(regex[, options])md5([bin])
示例
require "resty.validation.ngx"
local validation = require "resty.validation"
local valid, value = validation.unescapeuri.crc32("https://github.com/")
local valid, value = validation:unescapeuri():crc32()("https://github.com/")
resty.validation.tz 扩展
这一组验证器和过滤器基于伟大的 luatz 库,由 @daurnimator 开发,是一个用于时间和日期操作的库。要使用此扩展,您只需:
require "resty.validation.tz"
它将猴子补丁提供的适配器到 resty.validation 中,目前包括:
totimetabletotimestamp
(这些都有工厂和无参数版本)
totimestamp 和 totimetable 过滤器与 HTML5 日期和日期时间输入字段配合良好。顾名思义,totimetable 返回 luatz 的 timetable,而 totimestamp 返回自 Unix 纪元(1970-01-01)以来的秒数(作为 Lua 数字)。
示例
require "resty.validation.tz"
local validation = require "resty.validation"
local valid, ts = validation.totimestamp("1990-12-31T23:59:60Z")
local valid, ts = validation.totimestamp("1996-12-19")
resty.validation.utf8 扩展
这一组验证器和过滤器基于伟大的 utf8rewind 库,由 Quinten Lansu 开发,是一个用 C 编写的系统库,旨在扩展默认字符串处理函数以支持 UTF-8 编码文本。它需要我的 LuaJIT FFI 包装器 lua-resty-utf8rewind 才能工作。当满足上述要求时,其他部分就简单了。要使用此扩展,您只需:
require "resty.validation.utf8"
它将猴子补丁提供的适配器到 resty.validation 中,目前包括:
utf8upperutf8lowerutf8title
(这些都有工厂和无参数版本)
还有一些工厂验证器/过滤器:
utf8normalize(form)utf8category(category)
utf8normalize 将 UTF-8 输入标准化为以下标准化格式之一:
C(或NFC)D(或NFD)KC(或NFKC)KD(或NFKD)
utf8category 检查输入字符串是否属于以下类别之一(因此,您可以认为它内置了多个验证器以处理 UTF-8 字符串验证):
LETTER_UPPERCASELETTER_LOWERCASELETTER_TITLECASELETTER_MODIFIERCASE_MAPPEDLETTER_OTHERLETTERMARK_NON_SPACINGMARK_SPACINGMARK_ENCLOSINGMARKNUMBER_DECIMALNUMBER_LETTERNUMBER_OTHERNUMBERPUNCTUATION_CONNECTORPUNCTUATION_DASHPUNCTUATION_OPENPUNCTUATION_CLOSEPUNCTUATION_INITIALPUNCTUATION_FINALPUNCTUATION_OTHERPUNCTUATIONSYMBOL_MATHSYMBOL_CURRENCYSYMBOL_MODIFIERSYMBOL_OTHERSYMBOLSEPARATOR_SPACESEPARATOR_LINESEPARATOR_PARAGRAPHSEPARATORCONTROLFORMATSURROGATEPRIVATE_USEUNASSIGNEDCOMPATIBILITYISUPPERISLOWERISALPHAISDIGITISALNUMISPUNCTISGRAPHISSPACEISPRINTISCNTRLISXDIGITISBLANKIGNORE_GRAPHEME_CLUSTER
示例
require "resty.validation.utf8"
local validation = require "resty.validation"
local valid, ts = validation:utf8category("LETTER_UPPERCASE")("TEST")
resty.validation.injection 扩展
这一组验证器和过滤器基于伟大的 libinjection 库,由 Nick Galbreath 开发,是一个 SQL/SQLI/XSS 词法分析器解析器分析器。它需要我的 LuaJIT FFI 包装器 lua-resty-injection 才能工作。当满足上述要求时,其他部分就简单了。要使用此扩展,您只需:
require "resty.validation.injection"
它将猴子补丁提供的适配器到 resty.validation 中,目前包括:
sqli,如果检测到 SQL 注入,则返回false,否则返回truexss,如果检测到跨站脚本注入,则返回false,否则返回true
示例
require "resty.validation.injection"
local validation = require "resty.validation"
local valid, ts = validation.sqli("test'; DELETE FROM users;")
local valid, ts = validation.xss("test <script>alert('XSS');</script>")
API
我不会详细介绍所有不同的验证器和过滤器,因为它们都遵循相同的逻辑,但我会展示一些一般的工作方式。
validation._VERSION
此字段包含验证库的版本,例如,它的值可以是 "2.5",表示该库的版本 2.5。
布尔值、值/错误验证...
那 ... 表示验证链。这用于定义单个验证器链。链的长度没有限制。它将始终返回布尔值(验证是否有效)。第二个返回值将是未返回 true 作为验证结果的过滤器的名称,或过滤后的值。
local v = require "resty.validation"
-- 以下意味着,创建一个验证器,检查输入是否为:
-- 1. 字符串
-- 如果是,则修剪字符串开头和结尾的空白:
-- 2. trim
-- 然后检查修剪后的字符串长度是否至少为 5 个字符(UTF-8):
-- 3. minlen(5)
-- 如果一切正常,将该字符串转换为大写
-- (尚未支持 UTF-8 的大写):
-- 4. upper
local myvalidator = v.string.trim:minlen(5).upper
-- 此示例将返回 false 和 "minlen"
local valid, value = myvalidator(" \n\t a \t\n ")
-- 此示例将返回 true 和 "ABCDE"
local valid, value = myvalidator(" \n\t abcde \t\n ")
每当验证器失败并返回 false 时,您不应将返回的值用于其他目的,除了错误报告。因此,链的工作方式是这样的。lua-resty-validation 不会尝试执行您指定的永远不会被使用的链,例如:
local v = require "resty.validation"
-- 输入值不可能同时是字符串和数字:
local myvalidator = v.string.number:max(3)
-- 但您可以这样写
-- (将输入视为字符串,尝试将其转换为数字,并检查它是否至多为 3):
local myvalidator = v.string.tonumber:max(3)
如您所见,这是一种定义单个可重用验证器的方法。例如,您可以预定义一组基本单一验证器链,并将其存储在自己的模块中,从中可以在应用程序的不同部分重用相同的验证逻辑。定义单个可重用验证器并在组验证器中重用它们是个好主意。
例如,假设您有一个名为 validators 的模块:
local v = require "resty.validation"
return {
nick = v.string.trim:minlen(2),
email = v.string.trim.email,
password = v.string.trim:minlen(8)
}
现在您在应用程序的某处有 register 函数:
local validate = require "validators"
local function register(nick, email, password)
local vn, nick = validate.nick(nick)
local ve, email = validate.email(email)
local vp, password = validate.password(password)
if vn and ve and vp then
-- 输入有效,处理 nick、email 和 password
else
-- 输入无效,nick、email 和 password 包含错误原因
end
end
这很快就会变得有点混乱,这就是我们有组验证器的原因。
table validation.new([验证器表])
此函数是组验证的起点。假设您有一个注册表单,要求您输入昵称、电子邮件(重复两次)和密码(重复两次)。
我们将重用在 validators 模块中定义的单个验证器:
local v = require "resty.validation"
return {
nick = v.string.trim:minlen(2),
email = v.string.trim.email,
password = v.string.trim:minlen(8)
}
现在,让我们在 forms 模块中创建可重用的组验证器:
local v = require "resty.validation"
local validate = require "validators"
-- 首先为每个表单字段创建单个验证器
local register = v.new{
nick = validate.nick,
email = validate.email,
email2 = validate.email,
password = validate.password,
password2 = validate.password
}
-- 接下来,我们为电子邮件和密码创建组验证器:
register:compare "email == email2"
register:compare "password == password2"
-- 最后,我们从这个表单模块返回
return {
register = register
}
现在,在应用程序的某处,您有这个 register 函数:
local forms = require "forms"
local function register(data)
local valid, fields, errors = forms.register(data)
if valid then
-- 输入有效,处理字段
else
-- 输入无效,处理字段和错误
end
end
-- 您可能会像这样调用它:
register{
nick = "test",
email = "[email protected]",
email2 = "[email protected]",
password = "qwerty123",
password2 = "qwerty123"
}
组验证器的一个伟大之处在于,您可以将字段和错误表 JSON 编码并返回给客户端。这在构建单页面应用程序时可能会很方便,您需要在客户端报告服务器端错误。在上面的示例中,fields 变量将如下所示(valid 将为 true,errors 将为 nil):
{
nick = {
unvalidated = false,
value = "test",
input = "test",
name = "nick",
valid = true,
invalid = false,
validated = true
},
email = {
unvalidated = false,
value = "[email protected]",
input = "[email protected]",
name = "email",
valid = true,
invalid = false,
validated = true
},
email2 = {
unvalidated = false,
value = "[email protected]",
input = "[email protected]",
name = "email2",
valid = true,
invalid = false,
validated = true
},
password = {
unvalidated = false,
value = "qwerty123",
input = "qwerty123",
name = "password",
valid = true,
invalid = false,
validated = true
},
password2 = {
unvalidated = false,
value = "qwerty123",
input = "qwerty123",
name = "password2",
valid = true,
invalid = false,
validated = true
}
}
这对于进一步处理并将字段作为 JSON 编码发送回客户端 JavaScript 应用程序非常有用,但通常这对于发送到后端层来说构造得太重。要获取简单的键值表,我们可以调用这个字段表:
local data = fields()
data 变量现在将包含:
{
nick = "test",
email = "[email protected]",
email2 = "[email protected]",
password = "qwerty123",
password2 = "qwerty123"
}
现在这是您可以发送的内容,例如在 Redis 或您拥有的任何数据库(抽象)层中。但是,这并不止于此,如果您的数据库层只对 nick、email 和 password 感兴趣(例如,去掉那些重复项),您甚至可以调用 data 表:
local realdata = data("nick", "email", "password")
realdata 现在将包含:
{
nick = "test",
email = "[email protected]",
password = "qwerty123"
}
field:accept(value)
对于字段,您可以调用 accept,其作用如下:
self.error = nil
self.value = value
self.valid = true
self.invalid = false
self.validated = true
self.unvalidated = false
field:reject(error)
对于字段,您可以调用 reject,其作用如下:
self.error = error
self.valid = false
self.invalid = true
self.validated = true
self.unvalidated = false
string field:state(invalid, valid, unvalidated)
在字段上调用 state 非常适合在 HTML 模板中嵌入验证结果,例如 lua-resty-template。以下是使用 lua-resty-template 的示例:
<form method="post">
<input class="{{ form.email:state('invalid', 'valid') }}"
name="email"
type="text"
placeholder="Email"
value="{{ form.email.input }}">
<button type="submit">Join</button>
</form>
因此,根据电子邮件字段的状态,这将为输入元素添加一个类(例如,使输入的边框变为红色或绿色)。我们在这里不关心未验证的状态(例如,当用户首次加载页面和表单时)。
变更
每个版本的更改记录在 Changes.md 文件中。
另见
- lua-resty-route — 路由库
- lua-resty-reqargs — 请求参数解析器
- lua-resty-session — 会话库
- lua-resty-template — 模板引擎
GitHub
您可以在 nginx-module-validation 的 GitHub 仓库 中找到此模块的其他配置提示和文档。