最近后台收到不少技术负责人的私信,语气里都透着股焦虑:“老板说服务器被CC打挂了,业务停摆半小时损失几十万,让我赶紧搞个防护方案,但预算批不下来,只让用免费或低成本的工具。”
这种场景太真实了。很多中小团队甚至大厂的边缘业务,往往在出事后才想起安全这回事。其实,CC攻击(Challenge Collapsar)并不是什么高深莫测的黑客技术,它本质上就是利用HTTP/HTTPS协议正常请求的特性,通过海量僵尸主机发起高频、耗资源的正常或半正常请求,把服务器CPU、内存或带宽撑爆。
既然预算有限,我们就不能指望买昂贵的硬件防火墙或顶级云厂商的付费WAF套餐。今天这篇指南,我不讲那些虚头巴脑的理论,直接上干货,带你用开源工具和合理的架构设计,搭建一套“小而美”的高可用防护体系。咱们既要省钱,又要让攻击者无机可乘。
第一道防线:让攻击者“进不来”——Nginx层的基础限流与抗D
绝大多数CC攻击,流量最终都会打到你的Web服务器(如Nginx、Apache)上。如果能在入口就挡住大部分恶意请求,后面的应用服务器就能喘口气。Nginx作为反向代理,是性价比最高的第一道关卡。
1. 核心配置:漏桶算法与IP频次限制
不要只依赖简单的deny,那容易被绕过。我们需要结合limit_req_zone和limit_conn_zone。
http {
# 定义一个名为one的共享内存区域,大小为10m,用于存储IP访问状态
# rate=1r/s 表示每个IP每秒只能处理1个请求
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
# 定义连接数限制,防止单个IP建立过多并发连接
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
listen 80;
server_name example.com;
# 全局连接限制:每个IP最多允许5个并发连接
limit_conn addr 5;
location / {
# 启用限流,burst=20表示允许突发处理20个请求,nodelay表示立即处理突发请求
# 如果超过速率且burst已满,则返回503
limit_req zone=one burst=20 nodelay;
proxy_pass http://backend_app;
}
# 针对API接口更严格的限制
location /api/ {
limit_req zone=one burst=5 nodelay;
proxy_pass http://backend_api;
}
# 自定义503错误页面,告诉用户稍后再试,而不是暴露服务器内部错误
error_page 503 @maintenance;
location @maintenance {
return 503 "Too many requests, please try again later.";
}
}
}
这里有个坑要注意: rate=1r/s 对于正常用户来说可能太低了。你需要根据业务的实际QPS(每秒查询率)来调整。比如,如果你的首页正常访问是每秒50次,那你至少要把基础速率调高,或者针对特定敏感路径(如登录接口 /login)进行严格限制,而对静态资源(如 /static/*)放宽限制。
2. 屏蔽异常User-Agent和Referer
很多CC攻击工具使用的是默认的、奇怪的User-Agent字符串。虽然高级攻击者会伪造,但过滤掉明显的垃圾流量能节省大量资源。
# 拒绝空的User-Agent
if ($http_user_agent ~* "") {
return 403;
}
# 拒绝常见的爬虫或非浏览器UA
if ($http_user_agent ~* (Scrapy|Curl|HttpClient|Java|Python|Go-http-client)) {
return 403;
}
# 防盗链,如果Referer不是来自你自己的域名,则拒绝图片请求
location ~* \.(jpg|jpeg|png|gif)$ {
valid_referers none blocked server_names *.example.com example.com;
if ($invalid_referer) {
return 403;
}
}
第二道防线:智能识别与动态拦截——Lua + OpenResty
当基础的Nginx限流不够用时,我们需要更灵活的逻辑。这时候,OpenResty(基于Nginx+Lua)就派上用场了。Lua脚本执行效率极高,可以在毫秒级完成复杂的判断逻辑。
1. 基于行为特征的动态封禁
我们可以编写一段Lua代码,监测某个IP在短时间内对特定URL的访问频率。如果超过阈值,不仅限流,还将其加入黑名单一段时间。
-- 假设这段代码放在 nginx.conf 的 http 块中,并通过 lua_shared_dict 共享内存
-- 这里展示一个简单的逻辑,实际生产环境建议封装成模块
local redis = require "resty.redis"
local cjson = require "cjson"
-- 初始化Redis连接(用于持久化黑名单和计数)
local function get_redis()
local red = redis:new()
red:set_timeout(1000) -- 1秒超时
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "failed to connect Redis: ", err)
return nil
end
return red
end
-- 检查并更新IP访问计数
local function check_and_ban_ip(ip, uri)
local red = get_redis()
if not red then
return false
end
local key = "cc_protect:" .. ip .. ":" .. uri
local count, err = red:get(key)
if err then
ngx.log(ngx.ERR, "redis error: ", err)
return false
end
local current_count = tonumber(count) or 0
-- 设置过期时间,每5分钟重置一次计数窗口
if current_count == 0 then
red:expire(key, 300)
end
current_count = current_count + 1
red:set(key, current_count)
-- 如果一分钟内超过100次请求(简化逻辑,实际应结合时间窗口)
if current_count > 100 then
-- 写入黑名单
local ban_key = "cc_blacklist:" .. ip
red:set(ban_key, "1")
red:expire(ban_key, 3600) -- 封禁1小时
ngx.log(ngx.ALERT, "IP " .. ip .. " banned for CC attack on " .. uri)
return true
end
return false
end
-- 在server块中使用
-- access_by_lua_block {
-- local ip = ngx.var.remote_addr
-- local uri = ngx.var.uri
-- if check_and_ban_ip(ip, uri) then
-- ngx.exit(403)
-- end
-- }
为什么用Redis? 因为Lua变量只在worker进程内有效,重启后丢失。Redis提供了集中式的状态管理,确保所有Nginx节点都能同步黑名单信息。即使你有多台Web服务器,也能实现统一的防护策略。
2. 验证码挑战(CAPTCHA)
对于疑似攻击但未完全确认的IP,直接封杀可能会误伤正常用户。更好的做法是弹出验证码。
你可以集成开源的验证码服务,如reCAPTCHA(需科学上网)或自建简单的图形验证码/滑块验证。当Lua检测到请求频率略高于阈值但低于封禁阈值时,返回一个特殊的HTML页面,要求用户提交正确的验证码才能继续访问业务接口。
location /verify {
# 这里可以调用后端API生成验证码图片或二维码
proxy_pass http://captcha_service;
}
第三道防线:架构层面的“脱敏”与“缓冲”
软件层的防护总有极限,当攻击流量达到Gbps级别时,单台服务器的CPU再高也扛不住。这时候,必须依靠架构设计来分担压力。
1. CDN的“清洗”作用
虽然我们要低成本,但CDN不一定非要买昂贵的。许多云服务提供商提供免费额度的CDN,或者使用像Cloudflare这样的服务商,其免费版已经包含了基础的DDoS防护和WAF功能。
关键点: 将DNS解析指向CDN,而不是直接指向你的源站IP。这样,攻击流量首先到达CDN节点,CDN节点会过滤掉大部分明显的恶意请求和低频扫描。只有合法的、经过CDN清洗后的流量才会回源到你的服务器。
注意: 一定要隐藏源站IP!一旦源站IP泄露,攻击者可以直接绕过CDN进行直连攻击。可以通过以下方式隐藏:
- 不直接在网站源码中引用源站IP的图片或资源。
- 配置CDN的回源地址为内网IP或专用域名。
- 定期更换源站IP,并在CDN端更新配置。
2. 动静分离与静态资源缓存
CC攻击往往针对动态接口(如搜索、登录、下单)。对于静态资源(JS、CSS、图片),尽量让它们从CDN或本地缓存中获取,不消耗后端应用服务器的资源。
# Nginx配置示例:长期缓存静态文件
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 30d;
add_header Cache-Control "public, immutable";
# 如果文件存在,直接从磁盘读取,不向后端代理
try_files $uri =404;
}
3. 应用层的多级缓存
在应用代码层面,引入Redis或Memcached作为多级缓存。对于热点数据,先查缓存,再查数据库。这不仅能加速响应,还能在攻击导致数据库连接池耗尽时,保护数据库不被拖垮。
第四道防线:监控与应急响应——早发现,早止损
没有监控的防护是盲人摸象。你需要知道什么时候正在遭受攻击,攻击源在哪里,以及防护效果如何。
1. 实时日志分析
不要等到第二天早上看日志才知道被打了。使用ELK(Elasticsearch, Logstash, Kibana)或更轻量的Prometheus + Grafana来实时收集Nginx访问日志。
关键指标监控:
- QPS突增: 对比历史同期数据,如果QPS突然翻倍,立即告警。
- 5xx错误率飙升: 说明后端服务已经不堪重负。
- 特定IP高频访问: 找出Top 100访问IP,检查是否有异常。
2. 自动化封禁脚本
结合前面的Lua+Redis方案,你可以编写一个简单的Python脚本,定时从Redis中读取高频IP,并通过iptables或Nginx配置动态更新黑名单。
import redis
import subprocess
import time
def update_firewall():
r = redis.Redis(host='localhost', port=6379, db=0)
# 获取所有被封禁的IP
blacklisted_ips = r.keys("cc_blacklist:*")
for ip_key in blacklisted_ips:
ip = ip_key.split(":")[1]
# 使用iptables添加规则,丢弃该IP的所有流量
# 注意:实际生产中需要更精细的规则,避免封禁自己
cmd = f"iptables -A INPUT -s {ip} -j DROP"
subprocess.call(cmd, shell=True)
print(f"Blocked IP: {ip}")
if __name__ == "__main__":
while True:
update_firewall()
time.sleep(60) # 每分钟更新一次
重要提示: 在执行自动封禁前,务必进行测试,确保不会误封自己的管理IP或CDN回源IP。
常见误区与避坑指南
- 以为封IP就能一劳永逸: CC攻击者使用的是僵尸网络,IP来源成千上万,且不断更换。单纯封IP效率极低,应该结合行为分析、验证码和限流策略。
- 过度依赖单一技术: 不要只靠Nginx限流,也不要只靠Redis。多层防御(CDN -> Nginx -> Lua -> 应用缓存)才是王道。
- 忽视日志记录: 即使被攻击,也要保留详细的访问日志。这些日志是事后溯源、优化防护策略的重要依据。
- 测试不充分: 在上线新的防护规则前,务必在测试环境中模拟CC攻击,观察对正常用户体验的影响。避免因为防护过严导致正常用户无法访问。
结语:安全是一个持续的过程
构建低成本的高可用防护体系,并不是说一蹴而就的事情。它需要你对业务有深入的理解,对流量特征有敏锐的洞察,并对各种开源工具有足够的掌握。
从今天的分享来看,核心思路就是:入口节流(Nginx/CDN)、智能识别(Lua/Redis)、架构缓冲(缓存/动静分离)、实时监控(Prometheus/Grafana)。这套组合拳下来,足以抵御绝大多数中小规模的CC攻击。
记住,安全防护的本质是与攻击者的成本博弈。当你的防护成本远低于攻击者的投入时,他们自然会选择放弃。希望这份指南能帮你在预算有限的情况下,守住业务的底线。如果有具体的配置问题,欢迎随时交流,我们一起探讨。
