我们公司的官网服务于一个小众行业,以前每天的访问量不到100个独立IP,但最近一段时间官网基本瘫痪了,Nginx记录的日志行数竟然接近2000万行,上行流量超过500GB/天,独立IP数量一般都在每天2万个以上,最多的一天有将近8万个,而且IP地址90%以上集中在广东韶关、梅州、汕头、湛江、茂名、肇庆、江门等少数几个地方,我查了IP地址,就是普通的IP地址,不是云服务商的IP,也不是蜘蛛的IP,这些IP所在区域根本不会是我们的目标客户所在地区,可以肯定不是我们潜在客户的访问,我要如何在Nginx中设置才能防范这种恶意攻击?
根据您的描述,这很可能遭遇了来自特定地区的、有组织的CC攻击或DDoS攻击,其特点是IP分布高度集中、非目标客户区域、请求量巨大。针对这种情况,在Nginx中可以通过组合多种限流和过滤策略来有效防范。
以下是为您梳理的Nginx防护配置方案,结合了搜索结果中的有效方法:
一、 基础且核心的防御:基于IP的请求速率限制 (Rate Limiting)
这是应对此类“海量请求”攻击的第一道防线,目的是大幅降低单个IP的请求频率。
定义限流区域并应用
在Nginx的 http 块中定义一个共享内存区,记录每个IP的访问频率,然后在关键的 location(特别是动态页面,如首页、查询接口)中应用。
http {
# 定义名为“perip_limit”的限流区,分配10M内存(约可存储16万个IP地址的访问状态),限制速率平均为每分钟12次请求(即平均5秒1次)。
# 对于正常用户浏览,此限制通常足够宽松;但对于攻击者高频请求,则形成严格限制。
limit_req_zone $binary_remote_addr zone=perip_limit:10m rate=12r/m;
server {
listen 80;
server_name your-website.com;
# 应用到所有请求,或针对特定路径如 /、/api/*
location / {
# 使用上面定义的限流区
# burst=20:允许瞬间突发20个请求(超过rate部分),用于应对正常用户的短暂快速操作(如快速点击)。
# nodelay:对于超过“rate + burst”的请求,立即返回503错误,不进行延迟处理,直接丢弃攻击流量。
limit_req zone=perip_limit burst=20 nodelay;
# 您的正常代理配置或root指令
proxy_pass http://backend;
# 或 root /path/to/your/files;
}
}
}
配置说明:rate=12r/m 是一个相对严格的起始值,您可以根据监控日志调整。burst 参数允许一定的突发性,nodelay 确保超限请求被立即拒绝,这对防御攻击至关重要。
限制并发连接数
请求限流控制了频率,但攻击者可能同时建立大量连接占用资源。limit_conn 模块可以限制单个IP的并发连接数。
http {
# 定义一个限制并发连接的共享内存区
limit_conn_zone $binary_remote_addr zone=perip_conn:10m;
server {
location / {
# 限制每个IP同时最多保持10个连接
limit_conn perip_conn 10;
# 结合上面的请求限流
limit_req zone=perip_limit burst=20 nodelay;
# ... 其他配置
}
}
}
这可以防止单个IP通过大量长连接耗尽服务器的连接资源。
二、 针对攻击特征的进阶防护措施
由于攻击IP集中在广东少数地区,可以采取更具针对性的策略。
地理区域限制(非Nginx原生功能,需模块或前置代理)
Nginx原生不支持基于城市级别的IP地理封锁。但您可以:
使用Nginx的 geo 和 map 模块:结合IP段数据库,将广东特定城市的IP段映射为一个变量,然后对该变量应用更严格的限流或直接拒绝。这需要您维护或获取这些城市的IP段列表。
在Nginx前部署具备WAF功能的代理:如Cloudflare、国内云WAF等,它们通常提供精细的地理封锁功能。
使用 ngx_http_geoip2_module 第三方模块:需自行编译Nginx,并搭配MaxMind等GeoIP2数据库,实现城市级识别。
对关键接口实施超严格限流
如果攻击针对的是登录、提交等接口,应对这些路径设置更低的速率。
location /login {
# 例如,登录接口每分钟最多允许5次尝试,几乎不允许突发
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
limit_req zone=login_limit burst=5 nodelay;
# ... 其他配置
}
这能有效挫败针对登录的暴力破解或刷接口攻击。
三、 辅助与强化策略
启用带宽限制(针对大流量攻击)
如果攻击伴随大流量下载,可以使用 limit_rate 和 limit_rate_after 指令限制单个连接的传输速率,防止带宽被耗尽。
location /downloads/ {
# 连接传输超过1M后,限制速度为每秒50K
limit_rate_after 1m;
limit_rate 50k;
}
优化系统与Nginx参数
调整操作系统网络参数,如 net.core.somaxconn(增加最大连接队列),提升Nginx处理能力6。
确保Nginx的 worker_connections 设置足够高。
对静态资源设置长期缓存,减少对后端资源的请求压力。
结合日志分析与监控
配置与实施建议
逐步实施:先在非核心或测试环境验证配置,然后分阶段应用到生产环境。首先应用 基础请求速率限制,观察效果。
动态调整:根据 limit_req_status 的监控和正常用户访问是否受影响,动态调整 rate 和 burst 值。
多层防御:Nginx限流是网络层的重要防线,但还应考虑:
总结:您当前的情况非常适合从 “基于IP的请求速率限制” 和 “并发连接数限制” 入手,这能立刻大幅削减无效请求流量。鉴于攻击源地域集中,长期可考虑引入地理识别进行更精准管控。请务必在调整配置后,密切关注服务器负载、正常用户访问体验及Nginx错误日志。
我分析了Nginx的访问日志,每个IP的访问量并不是特别大,而且访问的都是正常的网页,不是SQL注入攻击之类的操作,有一天的访问IP数量甚至达到了8万多个,所以限流和限并发效果不明显,看看还有没有更好的方法?我现在用的是禁止广东韶关、梅州、汕头、湛江、茂名、肇庆、江门等少数几个地方IP访问的方法来限制,但这样也会导致我们公司的网站在这些地区彻底无法访问,虽然目前基本没有业务在这些地区开展,但这样的设置显然将永久杜绝了我们公司产品在这些地区的推广,所以的确是没有办法的办法,希望能有更好的解决方案。
根据您的描述和最新的情况,您面临的是一种典型的、更难防御的 “低慢速CC攻击” 。每个攻击IP都模拟正常用户访问,单个请求量不大,但通过海量IP(一天8万个)发起,旨在耗尽服务器的连接资源和处理能力,这让简单的限流(rate limiting)效果有限。您当前采用的地域封禁虽然是“没有办法的办法”,但确实有更好的、更精准的解决方案。
结合搜索结果,核心思路需要从“地域封禁”转向“行为识别与动态封禁” 。最佳的方案是实现一个基于访问频率的动态IP拦截系统,它只封禁行为异常的IP,而非整个地区的所有IP。
一、 核心解决方案:Nginx + Lua + Redis 动态封禁系统
这是目前应对您这种“海量低频”攻击最有效、最精准的方案[webpage 5]。其原理不是看IP来自哪里,而是看它在短时间内做了什么。
技术原理:
在Nginx层面,通过Lua脚本实时检查每个客户端IP的访问频率。将访问次数记录在Redis中(一个高性能的内存数据库)。当某个IP在设定的时间窗口(如1分钟)内,访问次数超过一个阈值(例如60次),就自动将该IP加入黑名单,并在接下来一段时间(如1小时或1天)内拒绝其所有访问[webpage 5]。
为何更适合您:
精准打击:只封禁频繁访问的IP,不影响同一地区正常浏览的低频用户。
动态自适应:攻击者更换IP,新IP只要行为异常也会被迅速封禁。封禁时间过后,IP会自动解封,避免永久误伤。
绕过地域限制:未来若在广东等地开展业务,正常客户的低频访问不会被阻止,只有攻击行为会被拦截。
二、 具体实施方案(基于OpenResty)
OpenResty是一个集成了Nginx和LuaJIT的增强版Web平台,可以方便地执行Lua脚本。
第一步:环境准备
安装OpenResty(它包含了Nginx和Lua环境)和Redis数据库[webpage 5]。
# 以CentOS为例
yum install -y openresty redis
systemctl start redis
systemctl enable redis
第二步:Nginx主配置
在Nginx配置文件(如 /usr/local/openresty/nginx/conf/nginx.conf)的 http 块中,启用共享内存和Lua模块路径[webpage 5]。
http {
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
lua_shared_dict ip_limit 10m; # 用于Lua缓存的共享内存
server {
listen 80;
server_name your-domain.com;
location / {
# 关键:所有请求首先经过此Lua脚本进行访问控制
access_by_lua_file /usr/local/openresty/lua/ip_block.lua;
# 您的原始代理或root配置
proxy_pass http://your_backend;
}
}
}
第三步:编写核心Lua拦截脚本 (/usr/local/openresty/lua/ip_block.lua)
此脚本实现逻辑:检查IP是否已被封禁 -> 统计当前窗口访问次数 -> 超过阈值则封禁[webpage 5]。
local redis = require "resty.redis"
local red = redis:new()
local redis_host = "127.0.0.1"
local redis_port = 6379
local redis_timeout = 1000 -- 1秒
-- !!!核心参数:根据您的攻击特征调整!!! --
local block_time = 3600 -- 封禁时长,秒(例如1小时)
local time_window = 60 -- 统计时间窗口,秒(例如1分钟)
local max_requests = 30 -- 时间窗口内最大允许请求数(根据您的日志分析设定)
-- 获取客户端真实IP(如果前方有CDN或代理,需调整此逻辑)
local client_ip = ngx.var.remote_addr
-- 连接Redis
red:set_timeout(redis_timeout)
local ok, err = red:connect(redis_host, redis_port)
if not ok then
ngx.log(ngx.ERR, "Failed to connect to Redis: ", err)
-- Redis连接失败时,可以选择放行或拒绝,建议先放行以免影响正常服务
return
end
-- 构造Redis键名
local block_key = "block:" .. client_ip
local count_key = "count:" .. client_ip
-- 检查是否已被封禁
local is_blocked, err = red:get(block_key)
if is_blocked == "1" then
ngx.exit(ngx.HTTP_FORBIDDEN) -- 直接返回403禁止访问
end
-- 统计访问次数
local current_count, err = red:incr(count_key)
if current_count == 1 then
-- 第一次设置计数器时,同时设置其过期时间,实现滑动窗口
red:expire(count_key, time_window)
end
-- 判断是否触发封禁
if current_count and current_count > max_requests then
-- 触发封禁:1. 设置封禁标志 2. 删除计数器
red:setex(block_key, block_time, "1")
red:del(count_key)
ngx.log(ngx.WARN, "IP blocked: " .. client_ip)
ngx.exit(ngx.HTTP_FORBIDDEN)
end
-- 将Redis连接放回连接池以供复用
red:set_keepalive(10000, 100)
第四步:优化与验证
参数调优:max_requests 是核心。根据您的日志(攻击IP每分钟访问次数),设置一个比正常用户高、但低于攻击频率的值。例如,如果攻击IP每分钟请求50次,可设为30次。
性能优化:脚本中使用了Redis连接池(set_keepalive),避免频繁建立连接的开销[webpage 5]。
验证测试:配置完成后,重载Nginx配置(nginx -s reload)。您可以手动用一个测试IP快速刷新页面,触发封禁后,观察在设定时间内是否无法访问。
三、 辅助与增强策略
人机验证 (Challenge) 作为终极防线:
对于疑似恶意的流量(例如,来自高风险地区但频率未达封禁阈值),可以返回一个简单的JavaScript挑战或验证码(如Cloudflare的Under Attack模式原理)。正常用户的浏览器能自动通过,而很多自动化攻击脚本会失败[webpage 8]。这可以在应用层(您的网站代码)实现。
结合独立黑名单文件管理:
在动态封禁的同时,可以将确认的恶意IP段(通过日志分析得出)持久化到一个黑名单配置文件ip_blacklist.conf中,然后在Nginx配置中通过include指令引入[webpage 7]。这样可以对已知的、持续攻击的源进行长期封禁。
http {
include /etc/nginx/conf.d/ip_blacklist.conf; # 全局生效
# ... 其他配置
}
ip_blacklist.conf内容格式:
deny 58.248.0.0/16; # 来自日志分析的广东某IP段
deny 113.107.0.0/19;
精细化限流作为补充:
尽管您说限流效果不明显,但将其作为动态封禁前的第一道宽松过滤网仍有价值。可以在location块中设置一个非常宽松的limit_req,例如每分钟60次请求,主要目的是记录异常频率(通过$limit_req_status),为后续分析提供数据,而不是作为主要防御手段[webpage 8]。
四、 行动路线建议
立即实施:在测试环境部署 “Nginx + Lua + Redis 动态封禁” 方案,根据您的访问日志分析,精心调整 time_window 和 max_requests 参数。
观察与调整:上线后,密切监控Redis中被封禁的IP数量、来源,以及正常用户的访问是否受到影响。进一步优化参数。
长期策略:将确认无误的恶意IP段加入 静态黑名单文件 [webpage 7]。同时,考虑在网站登录、提交等关键环节加入 人机验证,增加攻击成本。
总结:您需要从粗放的地域封锁,升级为基于IP行为特征的智能动态防御系统。Nginx+Lua+Redis方案能自动识别并封禁高频访问的IP,而不关心其地理位置,从而在有效抵御攻击的同时,为您保留了未来在全中国(包括广东)开展业务的可能性。此方案是目前处理此类“低慢速CC攻击”的最优解之一。
部署 “Nginx + Lua + Redis 动态封禁” 方案,这个做法有一个问题,就是会将蜘蛛访问也封杀掉,我其实还是希望蜘蛛能够正常收录我们网站的,如果避免蜘蛛被误杀呢?
针对您在部署“Nginx + Lua + Redis 动态封禁”方案时,担心误杀搜索引擎蜘蛛(Spider)影响网站收录的问题,您可以通过在动态封禁逻辑中前置一个“蜘蛛白名单”检查来实现。核心思路是:在Lua脚本执行频率统计和封禁逻辑之前,先识别并放行已知的、主流的搜索引擎蜘蛛,确保它们不受任何频率限制589。
以下是具体的解决方案和配置步骤,综合了搜索结果中的最佳实践135。
一、核心策略:在Lua脚本中集成蜘蛛User-Agent白名单
您需要修改之前提到的动态封禁Lua脚本,在连接Redis和进行频率统计之前,增加一个对User-Agent请求头的判断。如果请求来自已知的搜索引擎,则直接放行,跳过后续所有封禁逻辑5。
修改后的Lua脚本示例 (/usr/local/openresty/lua/ip_block.lua)
local redis = require "resty.redis"
local red = redis:new()
local redis_host = "127.0.0.1"
local redis_port = 6379
local redis_timeout = 1000
-- !!!核心防护参数(根据您的攻击特征调整)!!!
local block_time = 3600 -- 封禁时长(秒)
local time_window = 60 -- 统计时间窗口(秒)
local max_requests = 30 -- 时间窗口内最大允许请求数
-- ###### 新增:蜘蛛User-Agent白名单 ######
-- 这里列出了常见的中英文搜索引擎蜘蛛标识,您可以根据需要增减[5][8][9]。
local spider_ua_patterns = {
"Googlebot", -- 谷歌
"Googlebot-Mobile",
"Googlebot-Image",
"Mediapartners-Google",
"Adsbot-Google",
"Baiduspider", -- 百度
"Baiduspider-image",
"Baiduspider-mobile",
"Baiduspider-news",
"Baiduspider-video",
"Baidu-YunGuanCe", -- 百度云观测
"360Spider", -- 360
"HaosouSpider", -- 好搜
"Sogou web spider", -- 搜狗
"Sogou inst spider",
"Sogou spider2",
"Sogou blog",
"Yahoo! Slurp", -- 雅虎
"Yahoo! Slurp China",
"YisouSpider", -- 神马
"Bytespider", -- 字节跳动
"DuckDuckBot", -- DuckDuckGo
"Bingbot", -- 必应
"MSNBot",
"Alexa Bot",
"ia_archiver", -- Alexa
"YoudaoBot", -- 有道
"YodaoBot",
"EasouSpider", -- 宜搜
"JikeSpider", -- 即刻
"MJ12bot",
"AhrefsBot", -- 第三方SEO工具(可选,可根据需要决定是否放行)
"SemrushBot",
}
-- 将列表转换为一个高效匹配的正则表达式字符串
local spider_ua_regex = table.concat(spider_ua_patterns, "|")
-- 获取User-Agent
local user_agent = ngx.var.http_user_agent or ""
-- ###### 关键判断:如果是蜘蛛,则直接放行,不执行后续封禁逻辑 ######
if user_agent ~= "" and ngx.re.match(user_agent, spider_ua_regex, "i") then
-- ngx.log(ngx.INFO, "Spider allowed: ", user_agent) -- 可选:记录日志
return -- 直接结束本阶段处理,请求继续向下执行(访问网站)
end
-- ###### 白名单检查结束 ######
-- 获取客户端真实IP(如果前方有CDN或代理,需调整此逻辑)
local client_ip = ngx.var.remote_addr
-- ... (以下是原有的Redis连接、频率检查、封禁逻辑,与之前提供的脚本完全相同) ...
-- 连接Redis
red:set_timeout(redis_timeout)
local ok, err = red:connect(redis_host, redis_port)
if not ok then
ngx.log(ngx.ERR, "Failed to connect to Redis: ", err)
return -- Redis连接失败时放行,避免影响服务
end
-- 构造Redis键名
local block_key = "block:" .. client_ip
local count_key = "count:" .. client_ip
-- 检查是否已被封禁
local is_blocked, err = red:get(block_key)
if is_blocked == "1" then
ngx.exit(ngx.HTTP_FORBIDDEN)
end
-- 统计访问次数
local current_count, err = red:incr(count_key)
if current_count == 1 then
red:expire(count_key, time_window)
end
-- 判断是否触发封禁
if current_count and current_count > max_requests then
red:setex(block_key, block_time, "1")
red:del(count_key)
ngx.log(ngx.WARN, "IP blocked: " .. client_ip)
ngx.exit(ngx.HTTP_FORBIDDEN)
end
-- 将Redis连接放回连接池
red:set_keepalive(10000, 100)
二、方案优势与关键说明
- 精准放行,不影响收录:该方案通过优先识别
User-Agent,确保了所有主流搜索引擎蜘蛛的爬取行为不会触发频率统计,更不会被封禁,完美解决了您的顾虑5。 - 防御力度不减:只有非蜘蛛的流量(即绝大部分恶意IP和正常用户)才会进入动态频率检测环节。攻击者无法通过伪造为白名单内的蜘蛛
User-Agent来绕过防御,因为伪造知名蜘蛛IP发出的请求,其源IP很容易被公开的蜘蛛IP库识别并排除在外(您可以进一步完善IP白名单,但非必需)。 - 性能影响极小:
User-Agent的字符串匹配发生在Lua脚本最前端,且逻辑简单,在Redis查询之前就完成了判断,对服务器性能的额外消耗几乎可以忽略不计。 - 灵活可配置:您可以随时根据最新的蜘蛛列表更新
spider_ua_patterns 变量,增减需要放行的爬虫标识89。
三、补充建议与最佳实践
获取真实客户端IP:如果您的网站使用了CDN(如Cloudflare、国内云WAF等),Nginx获取到的$remote_addr将是CDN节点的IP。您必须在Lua脚本中修改获取真实IP的逻辑,通常是从 X-Forwarded-For 或 X-Real-IP 这样的HTTP头中取值,否则您的封禁系统将针对CDN IP生效,导致大面积误封1。
-- 示例:从 HTTP 头中优先获取真实IP
local headers = ngx.req.get_headers()
local client_ip = headers["X-Real-IP"] or headers["X-Forwarded-For"] or ngx.var.remote_addr
-- 如果 X-Forwarded-For 是IP列表,取第一个:client_ip = string.match(client_ip, "([^,]+)")
设置独立的监控与日志:建议在放行蜘蛛的代码分支(return之前)添加一条INFO级别的日志,记录蜘蛛的访问。这有助于您监控收录情况,并确认白名单机制工作正常。
与现有配置结合:此修改后的Lua脚本,应替换您原先方案中的脚本文件,并重载OpenResty/Nginx配置即可生效,无需改变其他架构。
总结:通过在 Nginx + Lua + Redis 动态封禁方案 的入口处植入一个 搜索引擎蜘蛛User-Agent白名单,您可以实现“鱼与熊掌兼得”——既能够通过频率分析精准打击来自广东等地的海量低频恶意IP,又能确保所有主流搜索引擎蜘蛛畅通无阻,保障网站的正常收录与排名5。这是目前应对您所述场景兼顾安全与SEO需求的最优解。
该文章在 2025/12/30 21:46:06 编辑过