first commit
This commit is contained in:
88
config/common_config.lua
Normal file
88
config/common_config.lua
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
-- 公共配置文件
|
||||||
|
-- 集中管理所有模块的通用配置
|
||||||
|
|
||||||
|
local _M = {}
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- IP速率限制配置
|
||||||
|
-- ============================================
|
||||||
|
_M.rate_limit = {
|
||||||
|
-- 是否启用
|
||||||
|
enabled = true,
|
||||||
|
|
||||||
|
-- 共享内存字典名称(需在nginx.conf中定义)
|
||||||
|
shared_dict = "ip_rate_limit",
|
||||||
|
|
||||||
|
-- 时间窗口(秒):统计多长时间内的请求
|
||||||
|
time_window = 60,
|
||||||
|
|
||||||
|
-- 最大请求次数:时间窗口内允许的最大请求数
|
||||||
|
max_requests = 100,
|
||||||
|
|
||||||
|
-- 处置策略:allow / rate_limit / block
|
||||||
|
action = "rate_limit",
|
||||||
|
|
||||||
|
-- HTTP状态码(根据action自动设置,也可手动指定)
|
||||||
|
status_code = nil,
|
||||||
|
|
||||||
|
-- 拒绝访问时的响应消息
|
||||||
|
message = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- User-Agent限制配置
|
||||||
|
-- ============================================
|
||||||
|
_M.ua_limit = {
|
||||||
|
-- 是否启用
|
||||||
|
enabled = false,
|
||||||
|
|
||||||
|
-- 处置策略:allow / rate_limit / block
|
||||||
|
action = "block",
|
||||||
|
|
||||||
|
-- HTTP状态码
|
||||||
|
status_code = 403,
|
||||||
|
|
||||||
|
-- 拒绝访问时的响应消息
|
||||||
|
message = "Access denied",
|
||||||
|
|
||||||
|
-- 黑名单User-Agent列表(匹配到的UA会被阻止)
|
||||||
|
blacklist = {
|
||||||
|
-- "BadBot",
|
||||||
|
-- "Scraper",
|
||||||
|
-- "Spider"
|
||||||
|
},
|
||||||
|
|
||||||
|
-- 白名单User-Agent列表(匹配到的UA总是放行)
|
||||||
|
whitelist = {
|
||||||
|
-- "Googlebot",
|
||||||
|
-- "Bingbot"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 域名配置(IP速率限制模块使用)
|
||||||
|
-- ============================================
|
||||||
|
_M.domains = {
|
||||||
|
-- 在此添加需要启用限流的域名
|
||||||
|
-- 留空表示对所有域名生效
|
||||||
|
-- 示例:
|
||||||
|
-- "example.com",
|
||||||
|
-- "api.example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 日志配置(所有模块共用)
|
||||||
|
-- ============================================
|
||||||
|
_M.log_config = {
|
||||||
|
-- 是否启用日志
|
||||||
|
enabled = true,
|
||||||
|
|
||||||
|
-- 日志级别:DEBUG, INFO, WARN, ERROR
|
||||||
|
log_level = "INFO",
|
||||||
|
log_path = "/usr/local/openresty/nginx/logs/waf.log",
|
||||||
|
|
||||||
|
log_request_details = true,
|
||||||
|
log_allowed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return _M
|
||||||
117
config/rate_limit_config.lua
Normal file
117
config/rate_limit_config.lua
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
-- IP速率限制统一配置模块
|
||||||
|
-- 从公共配置加载,提供便捷的访问接口
|
||||||
|
|
||||||
|
local _M = {}
|
||||||
|
|
||||||
|
local common_config = require "config.common_config"
|
||||||
|
|
||||||
|
-- 处置策略枚举
|
||||||
|
_M.ACTION_ALLOW = "allow" -- 放行
|
||||||
|
_M.ACTION_RATE_LIMIT = "rate_limit" -- 限制速率(返回429)
|
||||||
|
_M.ACTION_BLOCK = "block" -- 封禁(返回403)
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 预设配置模板(可选使用)
|
||||||
|
-- ============================================
|
||||||
|
_M.presets = {
|
||||||
|
-- 宽松模式:放行
|
||||||
|
allow_all = {
|
||||||
|
time_window = 60,
|
||||||
|
max_requests = 10000,
|
||||||
|
action = _M.ACTION_ALLOW
|
||||||
|
},
|
||||||
|
|
||||||
|
-- 标准模式:限制速率
|
||||||
|
standard = {
|
||||||
|
time_window = 60,
|
||||||
|
max_requests = 100,
|
||||||
|
action = _M.ACTION_RATE_LIMIT
|
||||||
|
},
|
||||||
|
|
||||||
|
-- 严格模式:封禁
|
||||||
|
strict = {
|
||||||
|
time_window = 60,
|
||||||
|
max_requests = 50,
|
||||||
|
action = _M.ACTION_BLOCK
|
||||||
|
},
|
||||||
|
|
||||||
|
-- API接口保护
|
||||||
|
api_protection = {
|
||||||
|
time_window = 60,
|
||||||
|
max_requests = 30,
|
||||||
|
action = _M.ACTION_RATE_LIMIT,
|
||||||
|
status_code = 429,
|
||||||
|
message = "API rate limit exceeded"
|
||||||
|
},
|
||||||
|
|
||||||
|
-- 登录防暴力破解
|
||||||
|
login_protection = {
|
||||||
|
time_window = 300,
|
||||||
|
max_requests = 10,
|
||||||
|
action = _M.ACTION_BLOCK,
|
||||||
|
message = "Too many login attempts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 配置管理函数
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 获取配置(从公共配置读取)
|
||||||
|
function _M.get_config()
|
||||||
|
return common_config.rate_limit
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 更新配置
|
||||||
|
function _M.update_config(new_config)
|
||||||
|
local cfg = common_config.rate_limit
|
||||||
|
for k, v in pairs(new_config) do
|
||||||
|
cfg[k] = v
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 如果没有手动指定状态码,根据action自动设置
|
||||||
|
if not new_config.status_code then
|
||||||
|
if cfg.action == _M.ACTION_RATE_LIMIT then
|
||||||
|
cfg.status_code = 429
|
||||||
|
elseif cfg.action == _M.ACTION_BLOCK then
|
||||||
|
cfg.status_code = 403
|
||||||
|
else
|
||||||
|
cfg.status_code = 200
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 如果没有手动指定消息,根据action自动设置
|
||||||
|
if not new_config.message then
|
||||||
|
if cfg.action == _M.ACTION_RATE_LIMIT then
|
||||||
|
cfg.message = "Rate limit exceeded. Please try again later."
|
||||||
|
elseif cfg.action == _M.ACTION_BLOCK then
|
||||||
|
cfg.message = "Access denied."
|
||||||
|
else
|
||||||
|
cfg.message = ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 应用预设配置
|
||||||
|
function _M.apply_preset(preset_name)
|
||||||
|
local preset = _M.presets[preset_name]
|
||||||
|
if preset then
|
||||||
|
_M.update_config(preset)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 快速配置函数
|
||||||
|
-- @param time_window: 时间窗口(秒)
|
||||||
|
-- @param max_requests: 最大请求次数
|
||||||
|
-- @param action: 处置策略(allow/rate_limit/block)
|
||||||
|
function _M.configure(time_window, max_requests, action)
|
||||||
|
_M.update_config({
|
||||||
|
time_window = time_window,
|
||||||
|
max_requests = max_requests,
|
||||||
|
action = action
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return _M
|
||||||
230
lib/logger.lua
Normal file
230
lib/logger.lua
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
-- 日志记录模块
|
||||||
|
-- 记录IP频率限制的详细日志信息
|
||||||
|
|
||||||
|
local _M = {}
|
||||||
|
|
||||||
|
-- 日志级别
|
||||||
|
_M.LOG_DEBUG = "DEBUG"
|
||||||
|
_M.LOG_INFO = "INFO"
|
||||||
|
_M.LOG_WARN = "WARN"
|
||||||
|
_M.LOG_ERROR = "ERROR"
|
||||||
|
|
||||||
|
-- 从公共配置加载日志配置
|
||||||
|
local common_config = require "config.common_config"
|
||||||
|
local config = common_config.log_config
|
||||||
|
|
||||||
|
-- 日志文件句柄缓存
|
||||||
|
config.log_file_handle = nil
|
||||||
|
|
||||||
|
-- 日志级别映射
|
||||||
|
local log_levels = {
|
||||||
|
DEBUG = ngx.DEBUG,
|
||||||
|
INFO = ngx.INFO,
|
||||||
|
WARN = ngx.WARN,
|
||||||
|
ERROR = ngx.ERR
|
||||||
|
}
|
||||||
|
|
||||||
|
-- 检查日志级别是否应该输出
|
||||||
|
local function should_log(level)
|
||||||
|
if not config.enabled then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local current_level = log_levels[config.log_level] or ngx.INFO
|
||||||
|
local msg_level = log_levels[level] or ngx.INFO
|
||||||
|
|
||||||
|
return msg_level >= current_level
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 清理字符串,防止日志注入
|
||||||
|
local function sanitize_string(str)
|
||||||
|
if not str then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 去除换行符、回车符等特殊字符
|
||||||
|
str = tostring(str)
|
||||||
|
str = str:gsub("[\r\n\t]", " ")
|
||||||
|
str = str:gsub("[%z\1-\31]", "")
|
||||||
|
|
||||||
|
-- 限制长度
|
||||||
|
if #str > 500 then
|
||||||
|
str = str:sub(1, 500) .. "..."
|
||||||
|
end
|
||||||
|
|
||||||
|
return str
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 获取当前时间戳
|
||||||
|
local function get_timestamp()
|
||||||
|
return ngx.time()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 格式化日志消息
|
||||||
|
local function format_message(level, message, extra_info)
|
||||||
|
local timestamp = get_timestamp()
|
||||||
|
local date_str = ngx.http_time(timestamp)
|
||||||
|
|
||||||
|
local log_msg = string.format("%s %s [%s] %s",
|
||||||
|
"[IP_RATE_LIMIT]",
|
||||||
|
date_str,
|
||||||
|
level,
|
||||||
|
message
|
||||||
|
)
|
||||||
|
|
||||||
|
if extra_info and next(extra_info) then
|
||||||
|
local extras = {}
|
||||||
|
for k, v in pairs(extra_info) do
|
||||||
|
table.insert(extras, string.format("%s=%s", k, sanitize_string(v)))
|
||||||
|
end
|
||||||
|
log_msg = log_msg .. " | " .. table.concat(extras, " ")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 添加换行符
|
||||||
|
log_msg = log_msg .. "\n"
|
||||||
|
|
||||||
|
return log_msg
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 打开日志文件
|
||||||
|
local function open_log_file()
|
||||||
|
if config.log_path == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 如果已经有打开的文件句柄,直接返回
|
||||||
|
if config.log_file_handle then
|
||||||
|
return config.log_file_handle
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 尝试打开文件(追加模式)
|
||||||
|
local file, err = io.open(config.log_path, "a")
|
||||||
|
if not file then
|
||||||
|
ngx.log(ngx.ERR, "[IP_RATE_LIMIT] Failed to open log file: ", config.log_path, " error: ", err)
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
config.log_file_handle = file
|
||||||
|
return file
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 关闭日志文件
|
||||||
|
local function close_log_file()
|
||||||
|
if config.log_file_handle then
|
||||||
|
config.log_file_handle:flush()
|
||||||
|
config.log_file_handle:close()
|
||||||
|
config.log_file_handle = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 写入日志
|
||||||
|
local function write_log(log_msg, ngx_level)
|
||||||
|
-- 如果配置了日志文件路径,写入文件
|
||||||
|
if config.log_path ~= "" then
|
||||||
|
local file = open_log_file()
|
||||||
|
if file then
|
||||||
|
file:write(log_msg)
|
||||||
|
file:flush()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 同时写入Nginx error.log(用于调试和备份)
|
||||||
|
ngx.log(ngx_level, log_msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 记录日志
|
||||||
|
local function log(level, message, extra_info)
|
||||||
|
if not should_log(level) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local log_msg = format_message(level, message, extra_info)
|
||||||
|
local ngx_level = log_levels[level] or ngx.INFO
|
||||||
|
|
||||||
|
write_log(log_msg, ngx_level)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 公共API
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 记录放行日志
|
||||||
|
function _M.log_allowed(client_ip, request_count, time_window)
|
||||||
|
if not config.log_allowed then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
log(_M.LOG_DEBUG, "Request allowed", {
|
||||||
|
ip = client_ip,
|
||||||
|
count = tostring(request_count),
|
||||||
|
window = tostring(time_window) .. "s",
|
||||||
|
action = "ALLOWED"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 记录限流日志
|
||||||
|
function _M.log_rate_limited(client_ip, request_count, max_requests, time_window)
|
||||||
|
log(_M.LOG_WARN, "Rate limit exceeded", {
|
||||||
|
ip = client_ip,
|
||||||
|
count = tostring(request_count),
|
||||||
|
max = tostring(max_requests),
|
||||||
|
window = tostring(time_window) .. "s",
|
||||||
|
action = "RATE_LIMITED",
|
||||||
|
status = "429"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 记录封禁日志
|
||||||
|
function _M.log_blocked(client_ip, request_count, max_requests, time_window)
|
||||||
|
log(_M.LOG_WARN, "Access blocked", {
|
||||||
|
ip = client_ip,
|
||||||
|
count = tostring(request_count),
|
||||||
|
max = tostring(max_requests),
|
||||||
|
window = tostring(time_window) .. "s",
|
||||||
|
action = "BLOCKED",
|
||||||
|
status = "403"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 记录请求详情
|
||||||
|
function _M.log_request_details(client_ip, uri, method, user_agent)
|
||||||
|
if not config.log_request_details then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
log(_M.LOG_DEBUG, "Request details", {
|
||||||
|
ip = client_ip,
|
||||||
|
uri = sanitize_string(uri),
|
||||||
|
method = method,
|
||||||
|
ua = sanitize_string(user_agent or "")
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 记录错误日志
|
||||||
|
function _M.log_error(message, error_info)
|
||||||
|
log(_M.LOG_ERROR, message, error_info)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 记录警告日志
|
||||||
|
function _M.log_warn(message, warn_info)
|
||||||
|
log(_M.LOG_WARN, message, warn_info)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 记录信息日志
|
||||||
|
function _M.log_info(message, info)
|
||||||
|
log(_M.LOG_INFO, message, info)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 刷新日志(确保所有日志已写入)
|
||||||
|
function _M.flush()
|
||||||
|
if config.log_file_handle then
|
||||||
|
config.log_file_handle:flush()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 清理资源(在worker退出时调用)
|
||||||
|
function _M.cleanup()
|
||||||
|
close_log_file()
|
||||||
|
end
|
||||||
|
|
||||||
|
return _M
|
||||||
151
modules/rate_limiter.lua
Normal file
151
modules/rate_limiter.lua
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
-- IP频率限制核心模块
|
||||||
|
-- 功能:统计指定IP在时间段内的请求次数,根据配置执行相应处置策略
|
||||||
|
|
||||||
|
local _M = {}
|
||||||
|
|
||||||
|
local config_module = require "config.rate_limit_config"
|
||||||
|
local common_config = require "config.common_config"
|
||||||
|
local logger = require "lib.logger"
|
||||||
|
|
||||||
|
-- 获取配置
|
||||||
|
local function get_config()
|
||||||
|
return config_module.get_config()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 检查域名是否应该应用限流
|
||||||
|
local function should_apply_rate_limit(host)
|
||||||
|
local cfg = get_config()
|
||||||
|
|
||||||
|
-- 如果未启用,直接返回false
|
||||||
|
if not cfg.enabled then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 如果没有host,默认不应用
|
||||||
|
if not host or host == "" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 从公共配置获取域名列表
|
||||||
|
local domains = common_config.domains
|
||||||
|
|
||||||
|
-- 如果domains列表为空,对所有域名生效
|
||||||
|
if #domains == 0 then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 检查域名是否在配置的列表中
|
||||||
|
for _, domain in ipairs(domains) do
|
||||||
|
if host == domain then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 主函数:检查并限制IP请求频率
|
||||||
|
-- 返回值:
|
||||||
|
-- - true: 允许访问
|
||||||
|
-- - false: 拒绝访问(已执行相应处置)
|
||||||
|
function _M.check_rate_limit()
|
||||||
|
local cfg = get_config()
|
||||||
|
|
||||||
|
-- 如果未启用,直接放行
|
||||||
|
if not cfg.enabled then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 获取当前域名
|
||||||
|
local host = ngx.var.host
|
||||||
|
|
||||||
|
-- 检查是否应该对此域名应用限流
|
||||||
|
if not should_apply_rate_limit(host) then
|
||||||
|
logger.log_info("Rate limit skipped for domain", {
|
||||||
|
domain = host,
|
||||||
|
reason = "not_in_domains_list"
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 获取共享内存字典
|
||||||
|
local dict = ngx.shared[cfg.shared_dict]
|
||||||
|
if not dict then
|
||||||
|
logger.log_error("Shared dictionary not found", {
|
||||||
|
dict_name = cfg.shared_dict
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 获取客户端IP
|
||||||
|
local client_ip = ngx.var.remote_addr
|
||||||
|
if not client_ip then
|
||||||
|
logger.log_warn("Failed to get client IP")
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 记录请求详情(可选)
|
||||||
|
logger.log_request_details(
|
||||||
|
client_ip,
|
||||||
|
ngx.var.request_uri,
|
||||||
|
ngx.var.request_method,
|
||||||
|
ngx.var.http_user_agent
|
||||||
|
)
|
||||||
|
|
||||||
|
local current_time = ngx.time()
|
||||||
|
local time_window = cfg.time_window
|
||||||
|
local max_requests = cfg.max_requests
|
||||||
|
|
||||||
|
-- 构建key:使用时间窗口的起始时间戳
|
||||||
|
local window_start = math.floor(current_time / time_window) * time_window
|
||||||
|
local key = client_ip .. ":" .. window_start
|
||||||
|
|
||||||
|
-- 使用原子操作增加计数
|
||||||
|
local new_count, err = dict:incr(key, 1, 0)
|
||||||
|
if not new_count then
|
||||||
|
logger.log_error("Failed to increment counter", {
|
||||||
|
ip = client_ip,
|
||||||
|
error = err or "unknown"
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 设置过期时间
|
||||||
|
if new_count == 1 then
|
||||||
|
dict:expire(key, time_window + 10)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 检查是否超过限制
|
||||||
|
if new_count > max_requests then
|
||||||
|
-- 根据配置的处置策略执行相应操作
|
||||||
|
if cfg.action == config_module.ACTION_ALLOW then
|
||||||
|
-- 放行:仅记录日志,不阻止
|
||||||
|
logger.log_allowed(client_ip, new_count, time_window)
|
||||||
|
return true
|
||||||
|
|
||||||
|
elseif cfg.action == config_module.ACTION_RATE_LIMIT then
|
||||||
|
-- 限制速率:返回429
|
||||||
|
logger.log_rate_limited(client_ip, new_count, max_requests, time_window)
|
||||||
|
|
||||||
|
ngx.status = cfg.status_code or 429
|
||||||
|
ngx.header["Content-Type"] = "text/plain; charset=utf-8"
|
||||||
|
ngx.say(cfg.message or "Rate limit exceeded")
|
||||||
|
return false
|
||||||
|
|
||||||
|
elseif cfg.action == config_module.ACTION_BLOCK then
|
||||||
|
-- 封禁:返回403
|
||||||
|
logger.log_blocked(client_ip, new_count, max_requests, time_window)
|
||||||
|
|
||||||
|
ngx.status = cfg.status_code or 403
|
||||||
|
ngx.header["Content-Type"] = "text/plain; charset=utf-8"
|
||||||
|
ngx.say(cfg.message or "Access denied")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 正常放行
|
||||||
|
logger.log_allowed(client_ip, new_count, time_window)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return _M
|
||||||
93
modules/ua_limiter.lua
Normal file
93
modules/ua_limiter.lua
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
-- User-Agent限制模块
|
||||||
|
-- 功能:根据User-Agent进行访问控制(黑白名单)
|
||||||
|
|
||||||
|
local _M = {}
|
||||||
|
|
||||||
|
local common_config = require "config.common_config"
|
||||||
|
local logger = require "lib.logger"
|
||||||
|
|
||||||
|
-- 获取配置
|
||||||
|
local function get_config()
|
||||||
|
return common_config.ua_limit
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 检查User-Agent是否在列表中(支持部分匹配)
|
||||||
|
local function ua_in_list(ua, list)
|
||||||
|
if not ua or #list == 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, pattern in ipairs(list) do
|
||||||
|
-- 使用字符串查找,支持部分匹配
|
||||||
|
if ua:find(pattern, 1, true) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 主函数:检查User-Agent
|
||||||
|
-- 返回值:
|
||||||
|
-- - true: 允许访问
|
||||||
|
-- - false: 拒绝访问
|
||||||
|
function _M.check_ua_limit()
|
||||||
|
local cfg = get_config()
|
||||||
|
|
||||||
|
-- 如果未启用,直接放行
|
||||||
|
if not cfg.enabled then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 获取User-Agent
|
||||||
|
local ua = ngx.var.http_user_agent
|
||||||
|
|
||||||
|
-- 如果没有UA,记录日志但不阻止(可根据需求调整)
|
||||||
|
if not ua or ua == "" then
|
||||||
|
logger.log_info("Empty User-Agent", {
|
||||||
|
ip = ngx.var.remote_addr,
|
||||||
|
uri = ngx.var.request_uri
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 检查白名单:在白名单中的UA总是放行
|
||||||
|
if ua_in_list(ua, cfg.whitelist) then
|
||||||
|
logger.log_info("User-Agent whitelisted", {
|
||||||
|
ua = ua,
|
||||||
|
action = "ALLOWED"
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 检查黑名单:在黑名单中的UA会被阻止
|
||||||
|
if ua_in_list(ua, cfg.blacklist) then
|
||||||
|
logger.log_warn("User-Agent blocked", {
|
||||||
|
ua = ua,
|
||||||
|
action = cfg.action,
|
||||||
|
status = tostring(cfg.status_code)
|
||||||
|
})
|
||||||
|
|
||||||
|
-- 执行处置策略
|
||||||
|
if cfg.action == "allow" then
|
||||||
|
-- 仅记录日志,不阻止
|
||||||
|
return true
|
||||||
|
elseif cfg.action == "rate_limit" then
|
||||||
|
ngx.status = cfg.status_code or 429
|
||||||
|
ngx.header["Content-Type"] = "text/plain; charset=utf-8"
|
||||||
|
ngx.say(cfg.message or "Rate limit exceeded")
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
-- 默认封禁
|
||||||
|
ngx.status = cfg.status_code or 403
|
||||||
|
ngx.header["Content-Type"] = "text/plain; charset=utf-8"
|
||||||
|
ngx.say(cfg.message or "Access denied")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 不在任何列表中,正常放行
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return _M
|
||||||
28
waf.lua
Normal file
28
waf.lua
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-- WAF统一入口模块
|
||||||
|
-- 功能:集中管理所有安全检查(IP限流、UA限制等)
|
||||||
|
|
||||||
|
local _M = {}
|
||||||
|
|
||||||
|
local rate_limiter = require "modules.rate_limiter"
|
||||||
|
local ua_limiter = require "modules.ua_limiter"
|
||||||
|
|
||||||
|
-- 执行所有安全检查
|
||||||
|
-- 返回值:
|
||||||
|
-- - true: 所有检查通过,允许访问
|
||||||
|
-- - false: 某个检查失败,已返回错误响应
|
||||||
|
function _M.check()
|
||||||
|
-- IP频率限制检查
|
||||||
|
if not rate_limiter.check_rate_limit() then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- User-Agent限制检查
|
||||||
|
if not ua_limiter.check_ua_limit() then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 所有检查通过
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return _M
|
||||||
Reference in New Issue
Block a user