lua-resty-validation
Lua 和 OpenResty 的验证库(输入验证和过滤)
$ opm get bungle/lua-resty-validation
lua-resty-validation
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"
-- Validators can be reused
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"
-- Validators can do filtering (i.e. modify the value being validated)
-- valid = true, s = "HELLO WORLD!"
local valid, s = validation.string.upper "hello world!"
-- You may extend the validation library with your own validators and filters...
validation.validators.capitalize = function(value)
return true, value:gsub("^%l", string.upper)
end
-- ... and then use it
local valid, e = validation.capitalize "abc" -- valid = true, e = "Abc"
-- You can also group validate many values
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("all the group fields are valid")
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
-- You can even call fields to get simple name, value table
-- (in that case all the `nil`s are removed as well)
-- By default this returns only the valid fields' names and values:
local data = fields()
local data = fields "valid"
-- To get only the invalid fields' names and values call:
local data = fields "invalid"
-- To get only the validated fields' names and values call (whether or not they are valid):
local data = fields "validated"
-- To get only the unvalidated fields' names and values call (whether or not they are valid):
local data = fields "unvalidated"
-- To get all, call:
local data = fields "all"
-- Or combine:
local data = fields("valid", "invalid")
-- This doesn't stop here. You may also want to get only some fields by their name.
-- You can do that by calling (returns a table):
local data = data{ "artist" }
安装
只需将 `validation.lua` 和 `validation` 目录放置在 package.path
中的 resty
目录下。如果你使用的是 OpenResty,默认位置为 /usr/local/openresty/lualib/resty
。
使用 OpenResty 包管理器 (opm)
$ opm get bungle/lua-resty-validation
使用 LuaRocks
$ luarocks install lua-resty-validation
lua-resty-validation
的 LuaRocks 仓库位于 https://luarocks.org/modules/bungle/lua-resty-validation。
内置验证器和过滤器
lua-resty-validation
附带了几个内置验证器,该项目欢迎更多验证器的贡献。
无参数的验证器和过滤器
类型验证器可用于验证被验证值的类型。这些验证器是无参数验证器(使用点 .
调用)。
null
或["nil"]
(因为 nil 是 Lua 中的保留字)boolean
number
string
table
userdata
func
或["function"]
(因为 function 是 Lua 中的保留字)callable
(函数或带有元方法__call
的表)thread
integer
float
file
(io.type(value) == 'file'
)
类型转换过滤器
tostring
tonumber
tointeger
toboolean
其他过滤器
tonil
或tonull
abs
inf
nan
finite
positive
negative
lower
upper
trim
ltrim
rtrim
reverse
email
optional
示例
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"]()
,检查值类型是否为nil
boolean()
,检查值类型是否为boolean
number()
,检查值类型是否为number
string()
,检查值类型是否为string
table()
,检查值类型是否为table
userdata()
,检查值类型是否为userdata
func()
或["function"]()
,检查值类型是否为function
callable()
,检查值是否可调用(即函数或带有元方法__call
的表)thread()
,检查值类型是否为thread
integer()
,检查值类型是否为integer
float()
,检查值类型是否为float
file()
,检查值类型是否为file
(io.type(value) == 'file'
)abs()
,过滤值并返回绝对值 (math.abs
)inf()
,检查值是否为inf
或-inf
nan()
,检查值是否为nan
finite()
,检查值是否不是nan
、inf
或-inf
positive()
,验证值是否为正 (> 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 betweeb 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)
-- or 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
这些大致等效
-- Both return: true, "default" (they stop prosessing :minlen(10) on nil and "" inputs
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)
-- now validate / filter the value, and return the results
-- here we just return false (aka making validation to fail)
return false
end)("Check this value"))
(当然,它不需要是内联函数,因为在 Lua 中所有函数都是一等公民,可以作为参数传递)
内置验证器扩展
目前 lua-resty-validation
支持两个可以启用的扩展或插件
resty.validation.ngx
resty.validation.tz
resty.validation.utf8
如果你想构建自己的验证器扩展,可以参考这些。如果你这样做,并且认为它也适合其他人使用,请考虑将你的扩展作为拉取请求提交到这个项目,非常感谢,;-)。
resty.validation.ngx 扩展
顾名思义,这组验证器扩展需要 OpenResty(至少需要 Lua Nginx 模块)。要使用此扩展,你只需
require "resty.validation.ngx"
它将修补它将在 resty.validation
中提供的适配器,这些适配器目前是
escapeuri
unescapeuri
base64enc
base64dec
crc32short
crc32long
crc32
md5
(这些都有工厂版本和无参数版本)
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
中提供的适配器,这些适配器目前是
totimetable
totimestamp
(这些都有工厂版本和无参数版本)
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 扩展
这组验证器和过滤器基于 Quinten Lansu 开发的优秀 `utf8rewind` 库 - 一个用 C 编写的系统库,旨在扩展默认字符串处理函数,以支持 UTF-8 编码文本。它需要我的 LuaJIT FFI 包装器 `lua-resty-utf8rewind` 才能工作。安装完上述要求后,剩下的就很简单了。要使用此扩展,你只需
require "resty.validation.utf8"
它将修补它将在 resty.validation
中提供的适配器,这些适配器目前是
utf8upper
utf8lower
utf8title
(这些都有工厂版本和无参数版本)
还有一些工厂验证器/过滤器
utf8normalize(form)
utf8category(category)
utf8normalize
将 UTF-8 输入规范化为以下规范化格式之一
C
(或NFC
)D
(或NFD
)KC
(或NFKC
)KD
(或NFKD
)
utf8category
检查输入字符串是否属于以下类别之一(因此,您可能会认为它内置了多个验证器来处理 UTF-8 字符串验证)
LETTER_UPPERCASE
LETTER_LOWERCASE
LETTER_TITLECASE
LETTER_MODIFIER
CASE_MAPPED
LETTER_OTHER
LETTER
MARK_NON_SPACING
MARK_SPACING
MARK_ENCLOSING
MARK
NUMBER_DECIMAL
NUMBER_LETTER
NUMBER_OTHER
NUMBER
PUNCTUATION_CONNECTOR
PUNCTUATION_DASH
PUNCTUATION_OPEN
PUNCTUATION_CLOSE
PUNCTUATION_INITIAL
PUNCTUATION_FINAL
PUNCTUATION_OTHER
PUNCTUATION
SYMBOL_MATH
SYMBOL_CURRENCY
SYMBOL_MODIFIER
SYMBOL_OTHER
SYMBOL
SEPARATOR_SPACE
SEPARATOR_LINE
SEPARATOR_PARAGRAPH
SEPARATOR
CONTROL
FORMAT
SURROGATE
PRIVATE_USE
UNASSIGNED
COMPATIBILITY
ISUPPER
ISLOWER
ISALPHA
ISDIGIT
ISALNUM
ISPUNCT
ISGRAPH
ISSPACE
ISPRINT
ISCNTRL
ISXDIGIT
ISBLANK
IGNORE_GRAPHEME_CLUSTER
示例
require "resty.validation.utf8"
local validation = require "resty.validation"
local valid, ts = validation:utf8category("LETTER_UPPERCASE")("TEST")
resty.validation.injection 扩展
这组验证器和过滤器基于 Nick Galbreath 的出色 `libinjection` 库,这是一个 SQL/SQLI/XSS 标记器解析器分析器。它需要我的 LuaJIT FFI 包装器 `lua-resty-injection` 才能工作。安装完上述要求后,剩下的就容易了。要使用此扩展,您只需
require "resty.validation.injection"
它将修补它将在 resty.validation
中提供的适配器,这些适配器目前是
sqli
,如果检测到 SQL 注入,则返回false
,否则返回true
xss
,如果检测到跨站点脚本注入,则返回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"
。
boolean,值/错误验证...
...
表示验证链。这用于定义单个验证器链。链长没有限制。它将始终返回布尔值(验证是否有效)。第二个返回值将是未返回 true
作为验证结果的过滤器的名称,或者过滤后的值。
local v = require "resty.validation"
-- The below means, create validator that checks that the input is:
-- 1. string
-- If, it is, then trim whitespaces from begin and end of the string:
-- 2. trim
-- Then check that the trimmed string's length is at least 5 characters (UTF-8):
-- 3. minlen(5)
-- And if everything is still okay, convert that string to upper case
-- (UTF-8 is not yet supported in upper):
-- 4. upper
local myvalidator = v.string.trim:minlen(5).upper
-- This example will return false and "minlen"
local valid, value = myvalidator(" \n\t a \t\n ")
-- This example will return true and "ABCDE"
local valid, value = myvalidator(" \n\t abcde \t\n ")
只要验证器失败并返回 false
,您就不应将返回的值用于除错误报告之外的任何其他目的。因此,链的工作方式如下。lua-resty-validation
不会尝试做任何事情,如果您指定永远不会使用的链,例如
local v = require "resty.validation"
-- The input value can never be both string and number at the same time:
local myvalidator = v.string.number:max(3)
-- But you could write this like this
-- (take input as a string, try to convert it to number, and check it is at most 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
-- input is valid, do something with nick, email, and password
else
-- input is invalid, nick, email, and password contain the error reasons
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"
-- First we create single validators for each form field
local register = v.new{
nick = validate.nick,
email = validate.email,
email2 = validate.email,
password = validate.password,
password2 = validate.password
}
-- Next we create group validators for email and password:
register:compare "email == email2"
register:compare "password == password2"
-- And finally we return from this forms module
return {
register = register
}
现在,您的应用程序中某个地方有这个 register
函数
local forms = require "forms"
local function register(data)
local valid, fields, errors = forms.register(data)
if valid then
-- input is valid, do something with fields
else
-- input is invalid, do something with fields and errors
end
end
-- And you might call it like:
register{
nick = "test",
email = "test@test.org",
email2 = "test@test.org",
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 = "test@test.org",
input = "test@test.org",
name = "email",
valid = true,
invalid = false,
validated = true
},
email2 = {
unvalidated = false,
value = "test@test.org",
input = "test@test.org",
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 = "test@test.org",
email2 = "test@test.org",
password = "qwerty123",
password2 = "qwerty123"
}
现在,您可以将其发送到例如 Redis 或您拥有的任何数据库(抽象)层。但是,这并没有就此结束,如果您的数据库层只对 nick
、email
和 password
感兴趣(例如,剥离这些重复项),您甚至可以调用 data
表
local realdata = data("nick", "email", "password")
realdata
现在将包含
{
nick = "test",
email = "test@test.org",
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 — 模板引擎
许可证
lua-resty-validation
使用双条款 BSD 许可证。
Copyright (c) 2014 - 2017, Aapo Talvensaari
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES`
作者
Aapo Talvensaari (@bungle)
许可证
2bsd
版本
-
用于 Lua 和 OpenResty 的验证库(输入验证和过滤) 2017-08-25 20:37:04
-
用于 Lua 和 OpenResty 的验证库(输入验证和过滤) 2017-02-05 19:06:36
-
用于 Lua 和 OpenResty 的验证库(输入验证和过滤) 2016-09-29 14:36:31