/usr/local/apisix/apisix/plugins/resource-matcher.lua
-- resource-matcher.lua
-- 资源匹配器插件 - 高效灵活的请求匹配和处理插件
-- 章亦春风格:高性能、简洁、错误处理完善
local core = require("apisix.core")
local ipairs = ipairs
local pairs = pairs
local type = type
local tostring = tostring
local ngx = ngx
local pcall = pcall
local ngx_var = ngx.var
local ngx_re = ngx.re
local tab_new = table.new
local tab_insert = table.insert
local tab_concat = table.concat
local str_find = string.find
local str_sub = string.sub
local str_match = string.match
local str_format = string.format
local str_lower = string.lower
local str_byte = string.byte
local math_random = math.random
-- 插件定义
local plugin_name = "resource-matcher"
-- 资源匹配器类型
local MATCHER_TYPES = {
URI = "uri",
HOST = "host",
USER_AGENT = "user_agent",
ARGS = "args",
REFERER = "referer",
METHOD = "method",
HEADER = "header",
COOKIE = "cookie",
IP = "ip",
CLIENT = "client"
}
-- 匹配操作符
local OPERATORS = {
EQ = "eq", -- 等于
NEQ = "neq", -- 不等于
MATCH = "match", -- 正则匹配
NOT_MATCH = "not_match", -- 正则不匹配
IN = "in", -- 在列表中
NOT_IN = "not_in", -- 不在列表中
PREFIX = "prefix", -- 前缀匹配
SUFFIX = "suffix", -- 后缀匹配
CONTAINS = "contains", -- 包含
NOT_CONTAINS = "not_contains", -- 不包含
EXISTS = "exists", -- 存在
NOT_EXISTS = "not_exists" -- 不存在
}
-- 动作类型
local ACTIONS = {
ALLOW = "allow", -- 允许请求
DENY = "deny", -- 拒绝请求
CHALLENGE = "challenge", -- 发起挑战
LIMIT = "limit", -- 限速
REWRITE = "rewrite", -- 重写请求
LOG = "log" -- 仅记录日志
}
-- 插件schema
local rule_schema = {
type = "object",
properties = {
name = {type = "string"},
priority = {type = "integer", default = 0},
conditions = {
type = "array",
items = {
type = "object",
properties = {
matcher = {
type = "string",
enum = {
"uri", "host", "user_agent", "args", "referer",
"method", "header", "cookie", "ip", "client"
}
},
operator = {
type = "string",
enum = {
"eq", "neq", "match", "not_match", "in", "not_in",
"prefix", "suffix", "contains", "not_contains",
"exists", "not_exists"
},
default = "eq"
},
key = {type = "string"},
value = {
oneOf = {
{type = "string"},
{type = "number"},
{type = "boolean"},
{type = "array"},
{type = "object"},
}
},
case_sensitive = {type = "boolean", default = false}
},
required = {"matcher"}
},
minItems = 1
},
logic = {
type = "string",
enum = {"AND", "OR", "NOT"},
default = "AND"
},
action = {
type = "string",
enum = {"allow", "deny", "challenge", "limit", "rewrite", "log"},
default = "allow"
},
action_params = {
type = "object",
properties = {
status_code = {type = "integer", minimum = 100, maximum = 599},
message = {type = "string"},
challenge_type = {
type = "string",
enum = {"js", "captcha", "progressive"}
},
limit_rate = {type = "integer", minimum = 1},
limit_burst = {type = "integer", minimum = 1},
rewrite_path = {type = "string"},
rewrite_host = {type = "string"},
log_level = {
type = "string",
enum = {"debug", "info", "notice", "warn", "error", "crit", "alert", "emerg"}
}
}
}
},
required = {"name", "conditions", "action"}
}
local schema = {
type = "object",
properties = {
rules = {
type = "array",
items = rule_schema,
default = {}
},
default_action = {
type = "string",
enum = {"allow", "deny", "challenge"},
default = "allow"
},
default_challenge_type = {
type = "string",
enum = {"js", "captcha", "progressive"},
default = "js"
},
debug = {type = "boolean", default = false}
}
}
local _M = {
version = 0.1,
priority = 2900, -- 比js_flowers优先级高,确保先执行
name = plugin_name,
schema = schema
}
-- 调试日志函数
local function debug_log(conf, ...)
if conf and conf.debug then
core.log.warn("[", plugin_name, "] ", ...)
end
end
-- 获取请求参数值
local function get_request_value(ctx, matcher, key)
if matcher == MATCHER_TYPES.URI then
return ctx.var.uri
elseif matcher == MATCHER_TYPES.HOST then
return ctx.var.host
elseif matcher == MATCHER_TYPES.USER_AGENT then
return ctx.var.http_user_agent or ""
elseif matcher == MATCHER_TYPES.REFERER then
return ctx.var.http_referer or ""
elseif matcher == MATCHER_TYPES.METHOD then
return ctx.var.request_method
elseif matcher == MATCHER_TYPES.ARGS then
local args = core.request.get_uri_args(ctx)
if key then
return args[key]
end
return args
elseif matcher == MATCHER_TYPES.HEADER then
if key then
return ctx.var["http_" .. key:gsub("-", "_"):lower()]
end
return nil
elseif matcher == MATCHER_TYPES.COOKIE then
local cookie = ctx.var.http_cookie or ""
if not key then
return cookie
end
local pattern = key .. "=([^;]+)"
local m = ngx_re.match(cookie, pattern, "jo")
if m then
return m[1]
end
return nil
elseif matcher == MATCHER_TYPES.IP then
return ctx.var.remote_addr
elseif matcher == MATCHER_TYPES.CLIENT then
local client_info = {
ip = ctx.var.remote_addr,
user_agent = ctx.var.http_user_agent or "",
host = ctx.var.host,
uri = ctx.var.uri
}
if key then
return client_info[key]
end
return client_info
end
return nil
end
-- 执行单个条件匹配
local function match_condition(condition, ctx)
local matcher = condition.matcher
local operator = condition.operator or OPERATORS.EQ
local key = condition.key
local expected = condition.value
local case_sensitive = condition.case_sensitive or false
-- 获取请求值
local actual = get_request_value(ctx, matcher, key)
-- 值不存在检查
if operator == OPERATORS.EXISTS then
return actual ~= nil and actual ~= ""
elseif operator == OPERATORS.NOT_EXISTS then
return actual == nil or actual == ""
end
-- 如果值不存在且不是检查存在性的操作,则匹配失败
if actual == nil then
return false
end
-- 如果不区分大小写,转换为小写
if not case_sensitive and type(actual) == "string" and type(expected) == "string" then
actual = str_lower(actual)
expected = str_lower(expected)
end
-- 执行匹配操作
if operator == OPERATORS.EQ then
return actual == expected
elseif operator == OPERATORS.NEQ then
return actual ~= expected
elseif operator == OPERATORS.MATCH then
local ok, res = pcall(ngx_re.match, actual, expected, "jo")
return ok and res ~= nil
elseif operator == OPERATORS.NOT_MATCH then
local ok, res = pcall(ngx_re.match, actual, expected, "jo")
return ok and res == nil
elseif operator == OPERATORS.IN then
if type(expected) == "table" then
for _, v in ipairs(expected) do
if actual == v then
return true
end
end
end
return false
elseif operator == OPERATORS.NOT_IN then
if type(expected) == "table" then
for _, v in ipairs(expected) do
if actual == v then
return false
end
end
end
return true
elseif operator == OPERATORS.PREFIX then
if type(actual) == "string" and type(expected) == "string" then
return str_sub(actual, 1, #expected) == expected
end
return false
elseif operator == OPERATORS.SUFFIX then
if type(actual) == "string" and type(expected) == "string" then
return #actual >= #expected and str_sub(actual, -#expected) == expected
end
return false
elseif operator == OPERATORS.CONTAINS then
if type(actual) == "string" and type(expected) == "string" then
return str_find(actual, expected, 1, true) ~= nil
elseif type(actual) == "table" then
for k, v in pairs(actual) do
if k == expected or v == expected then
return true
end
end
end
return false
elseif operator == OPERATORS.NOT_CONTAINS then
if type(actual) == "string" and type(expected) == "string" then
return str_find(actual, expected, 1, true) == nil
elseif type(actual) == "table" then
for k, v in pairs(actual) do
if k == expected or v == expected then
return false
end
end
end
return true
end
return false
end
-- 评估规则条件
local function evaluate_conditions(conditions, logic, ctx)
if not conditions or #conditions == 0 then
return true
end
if logic == "NOT" and #conditions == 1 then
return not match_condition(conditions[1], ctx)
end
if logic == "OR" then
for _, condition in ipairs(conditions) do
if match_condition(condition, ctx) then
return true
end
end
return false
else -- 默认为AND
for _, condition in ipairs(conditions) do
if not match_condition(condition, ctx) then
return false
end
end
return true
end
end
-- 执行规则动作
local function execute_action(rule, ctx)
local action = rule.action
local params = rule.action_params or {}
-- 记录匹配的规则
ctx.matched_resource_rule = rule.name
if action == ACTIONS.ALLOW then
ctx.resource_matcher_allow = true
debug_log(ctx.conf, "rule [", rule.name, "] matched, allowing request")
return nil
elseif action == ACTIONS.DENY then
debug_log(ctx.conf, "rule [", rule.name, "] matched, denying request")
return {
status = params.status_code or 403,
message = params.message or "Forbidden by resource matcher"
}
elseif action == ACTIONS.CHALLENGE then
ctx.resource_matcher_challenge = true
ctx.resource_matcher_challenge_type = params.challenge_type or ctx.conf.default_challenge_type or "js"
debug_log(ctx.conf, "rule [", rule.name, "] matched, challenge required: ", ctx.resource_matcher_challenge_type)
return nil
elseif action == ACTIONS.LIMIT then
ctx.resource_matcher_limit = true
ctx.resource_matcher_limit_rate = params.limit_rate or 10
ctx.resource_matcher_limit_burst = params.limit_burst or 5
debug_log(ctx.conf, "rule [", rule.name, "] matched, limiting request: rate=",
ctx.resource_matcher_limit_rate, ", burst=", ctx.resource_matcher_limit_burst)
return nil
elseif action == ACTIONS.REWRITE then
if params.rewrite_path then
ctx.var.uri = params.rewrite_path
end
if params.rewrite_host then
ctx.var.upstream_host = params.rewrite_host
end
debug_log(ctx.conf, "rule [", rule.name, "] matched, rewriting request")
return nil
elseif action == ACTIONS.LOG then
local log_level = params.log_level or "warn"
core.log[log_level]("resource-matcher rule [", rule.name, "] matched for URI: ", ctx.var.uri)
return nil
end
return nil
end
-- 评估所有规则
local function evaluate_rules(conf, ctx)
-- 排序规则,按优先级降序排列
local rules = conf.rules
if not rules or #rules == 0 then
return nil
end
-- 创建规则副本并排序
local sorted_rules = tab_new(#rules, 0)
for i, rule in ipairs(rules) do
sorted_rules[i] = rule
end
table.sort(sorted_rules, function(a, b)
return (a.priority or 0) > (b.priority or 0)
end)
-- 评估每个规则
for _, rule in ipairs(sorted_rules) do
local matched = evaluate_conditions(rule.conditions, rule.logic, ctx)
if matched then
return execute_action(rule, ctx)
end
end
-- 没有规则匹配,使用默认动作
if conf.default_action == ACTIONS.DENY then
return {
status = 403,
message = "Forbidden by default"
}
elseif conf.default_action == ACTIONS.CHALLENGE then
ctx.resource_matcher_challenge = true
ctx.resource_matcher_challenge_type = conf.default_challenge_type or "js"
debug_log(conf, "no rule matched, default challenge required: ", ctx.resource_matcher_challenge_type)
end
return nil
end
-- 处理拒绝请求
local function handle_denied_request(ctx, result)
local status = result.status or 403
local message = result.message or "Forbidden"
ngx.status = status
ngx.header["Content-Type"] = "text/html; charset=utf-8"
-- 构建友好的错误页面
local html = [[
访问受限
]] .. message .. [[
如果您认为这是一个错误,请联系网站管理员。
ngx.say(html)
return ngx.exit(status)
end
-- 检查schema
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
-- 插件主函数:rewrite阶段
function _M.rewrite(conf, ctx)
-- 保存配置到上下文,供其他函数使用
ctx.conf = conf
-- 评估规则
local result = evaluate_rules(conf, ctx)
-- 处理结果
if result then
return handle_denied_request(ctx, result)
end
end
-- 插件主函数:access阶段
function _M.access(conf, ctx)
-- 如果已经在rewrite阶段处理过,则跳过
if ctx.resource_matcher_processed then
return
end
-- 标记为已处理
ctx.resource_matcher_processed = true
-- 如果需要限速
if ctx.resource_matcher_limit then
-- 这里可以实现限速逻辑,或者与limit-req插件集成
-- 简单示例:随机丢弃一些请求
if math_random(100) <= 100 / ctx.resource_matcher_limit_rate then
return
else
return 429, {message = "Too Many Requests"}
end
end
end
-- 插件初始化函数
function _M.init()
-- 可以在这里进行插件初始化工作
core.log.info(plugin_name, " plugin initialized")
end
-- 控制API
function _M.control_api()
return {
{
methods = {"GET"},
uris = {"/v1/plugin/resource-matcher/status"},
handler = function()
return 200, {status = "ok", version = _M.version}
end
}
}
end
return _M

浙公网安备 33010602011771号