跳转至

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 中的保留关键字)
  • boolean
  • number
  • string
  • table
  • userdata
  • func["function"](因为 function 是 Lua 中的保留关键字)
  • callable(可以是一个函数或一个具有元方法 __call 的表)
  • thread
  • integer
  • float
  • fileio.type(value) == 'file'

类型转换过滤器:

  • tostring
  • tonumber
  • tointeger
  • toboolean

其他过滤器:

  • toniltonull
  • 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(),检查值类型是否为 fileio.type(value) == 'file'
  • abs(),过滤值并返回绝对值(math.abs
  • inf(),检查值是否为 inf-inf
  • nan(),检查值是否为 nan
  • finite(),检查值是否不是 naninf-inf
  • positive(),验证值是否为正(> 0
  • negative(),验证值是否为负(< 0
  • min(min),验证值至少为 min>=
  • max(max),验证值至多为 max<=
  • between(min[, max = min]),验证值在 minmax 之间
  • outside(min[, max = min]),验证值不在 minmax 之间
  • divisible(number),验证值是否能被 number 整除
  • indivisible(number),验证值是否不能被 number 整除
  • len(min[, max = min]),验证值的长度是否恰好为 min 或在 minmax 之间(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(),将值转换为 nil
  • lower(),将值转换为小写(尚未实现 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,并返回 defaultvalue

条件验证工厂验证器

对于所有验证工厂验证器,都有一个条件版本,始终验证为 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)

条件验证工厂验证器的最后两个参数是 truthyfalsy 值。其他每个参数都传递给实际的验证工厂验证器。

组验证器

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 关系运算符:

  • <
  • >
  • <=
  • >=
  • ==
  • ~=

requisiterequisites 检查字段值是否为 nil""(空字符串)。使用 requisite,如果所有指定字段都是 nil"",则所有字段都是无效的(前提是它们本身不是无效的),如果至少有一个字段有效,则所有字段都是有效的。requisites 的工作方式相同,但您可以定义希望有多少字段的值不是 nil 且不是空字符串 ""。这些提供了条件验证的意义:

  1. 我有(两个或更多)字段
  2. 所有字段都是可选的
  3. 至少一个/定义数量的字段应该被填充,但我不在乎哪个,只要至少有一个/定义数量的字段被填充

停止验证器

停止验证器,如 optional,与普通验证器类似,但它们不是返回 truefalse 作为验证结果或过滤值,而是可以返回 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

大多数不过滤值的验证器仅返回 truefalse 作为结果。这意味着现在没有办法向 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.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

(这些都有工厂和无参数版本)

totimestamptotimetable 过滤器与 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 中,目前包括:

  • 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 扩展

这一组验证器和过滤器基于伟大的 libinjection 库,由 Nick Galbreath 开发,是一个 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。

布尔值、值/错误验证...

... 表示验证链。这用于定义单个验证器链。链的长度没有限制。它将始终返回布尔值(验证是否有效)。第二个返回值将是未返回 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 或您拥有的任何数据库(抽象)层中。但是,这并不止于此,如果您的数据库层只对 nickemailpassword 感兴趣(例如,去掉那些重复项),您甚至可以调用 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 文件中。

另见

GitHub

您可以在 nginx-module-validation 的 GitHub 仓库 中找到此模块的其他配置提示和文档。