first commit

This commit is contained in:
2026-05-06 17:36:25 +08:00
commit c9c2fa8185
6 changed files with 707 additions and 0 deletions

88
config/common_config.lua Normal file
View 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

View 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
View 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
View 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
View 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
View 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