个人站点被CC攻击打挂,最直观的表现不是带宽跑满,而是服务器CPU瞬间飙升到100%,数据库连接数耗尽,网站无法打开。很多人第一反应是花钱买高防IP或者上CDN,但对于一个月几十块钱预算的站长来说,那套商业方案根本不现实。低成本防护的核心思路不是硬抗,而是用极小的性能代价识别并丢弃恶意请求,把正常流量放进来。

理解CC攻击的本质才能低成本防御

CC攻击不同于流量洪水型的DDoS,它往往只需要几百个甚至几十个代理IP,模拟真实用户不断请求那些消耗服务器资源最大的页面,比如搜索接口、数据库写入接口、复杂的动态页面。攻击者用极低的成本就能让你的服务器资源耗尽。所以防御的关键在于:在请求到达业务逻辑之前,就判断出谁是恶意请求并直接拒绝服务。这个判断要足够轻量,不能反过来消耗更多资源。

Nginx限流是最基础也是最重要的一层

绝大多数个人站点都在用Nginx,而Nginx自带的两个模块就能挡掉相当一部分CC攻击。第一个是ngx_http_limit_req_module,用来限制单个IP的请求频率。配置思路不是一刀切地限制所有请求,而是对敏感URL做精准限制。比如你的登录接口、搜索接口、评论提交接口,这些地方消耗资源大,正常用户访问频率很低,可以设置一个很低的阈值。在Nginx配置里这样写:

http {
    limit_req_zone $binary_remote_addr zone=login:10m rate=3r/m;
    limit_req_zone $binary_remote_addr zone=search:10m rate=10r/m;
    
    server {
        location /login {
            limit_req zone=login burst=2 nodelay;
            # 你的后端代理配置
        }
        location /search {
            limit_req zone=search burst=5 nodelay;
        }
    }
}

这里rate=3r/m表示每分钟最多3个请求,burst=2允许短时突发多2个,nodelay表示超出立即返回503,不给攻击者排队等待的机会。正常用户登录一分钟点不了3次,搜索也不会一分钟搜10次,这个阈值对真实用户几乎无感,但能直接废掉那些用代理池循环请求的攻击脚本。第二个模块是ngx_http_limit_conn_module,限制单个IP的并发连接数,在http块里定义共享内存区域,然后在server或location里应用,单个IP并发连接超过阈值就直接拒绝,防止攻击者用少量IP建立大量连接耗尽worker进程。

利用iptables做IP层的快速丢弃

Nginx限流虽然有效,但恶意请求还是走到了应用层,消耗了Nginx的worker进程和部分系统资源。更彻底的方案是在网络层就直接把攻击IP丢掉。iptables的recent模块可以跟踪IP的访问频率,配合hashlimit模块实现类似效果。一条实用的规则链是:新建一个名为CC-DEFENSE的链,把所有入站80和443端口的SYN包先转到这个链里处理,记录每个IP在60秒内的新连接数,超过20个就写入一个黑名单集合,后续这个IP的所有包直接DROP。命令如下:

iptables -N CC-DEFENSE
iptables -A INPUT -p tcp --dport 80 -m state --state NEW -j CC-DEFENSE
iptables -A INPUT -p tcp --dport 443 -m state --state NEW -j CC-DEFENSE
iptables -A CC-DEFENSE -m recent --set --name CC_CHECK
iptables -A CC-DEFENSE -m recent --update --seconds 60 --hitcount 20 --name CC_CHECK -j SET --add-set blacklist src
iptables -A CC-DEFENSE -m set --match-set blacklist src -j DROP

这里用到了ipset来存储黑名单,比直接用iptables规则逐条添加效率高得多。先创建ipset集合blacklist,超时时间设成3600秒,一小时后自动解封,避免误封正常用户太久。ipset支持哈希存储,查询复杂度是O(1),即使黑名单里有几万个IP,匹配速度也几乎不受影响。这套组合拳下来,攻击IP在发送第21个SYN包的时候就会被内核直接丢弃,连TCP握手都完不成,对服务器资源的消耗降到最低。

WAF层面的智能识别不能少

单纯的频率限制有个致命缺陷:攻击者如果使用大量代理IP,每个IP的请求频率都很低,就能绕过阈值。这时候需要从请求特征入手。开源WAF里ModSecurity配合OWASP核心规则集是一个选择,但ModSecurity太重了,个人站点的低配机器跑起来会很吃力。更轻量的方案是用Nginx+Lua,通过OpenResty或者直接给Nginx打lua-nginx-module模块,写几行Lua脚本来做请求特征分析。比如检测User-Agent是否为空或者是一些常见的攻击脚本特征,检测请求头里Referer是否来自本站,检测请求参数里是否包含大量随机字符串。这些检测逻辑在Nginx的access阶段执行,性能损耗极小。一个简单的Lua脚本示例:

access_by_lua_block {
    local ua = ngx.var.http_user_agent
    if not ua or ua == "" then
        ngx.exit(403)
    end
    
    local bad_agents = {"sqlmap", "nikto", "acunetix", "nmap", "masscan"}
    for _, v in ipairs(bad_agents) do
        if string.find(string.lower(ua), v) then
            ngx.exit(403)
        end
    end
    
    local referer = ngx.var.http_referer
    if referer and not string.find(referer, "你的域名") then
        -- 对某些敏感接口检查Referer
        if ngx.var.uri == "/api/submit" then
            ngx.exit(403)
        end
    end
}

这段脚本在每次请求时执行,检查UA是否为空或包含已知攻击工具名,对敏感接口校验Referer来源。逻辑很简单,但能过滤掉大量脚本化的CC攻击,因为攻击脚本往往不会精心构造完整的请求头。更进阶的做法是结合cookie挑战机制:当某个IP的请求频率超过阈值但还没到封禁程度时,返回一个302重定向要求客户端设置cookie,后续请求必须携带这个cookie才能访问。正常浏览器会自动处理cookie重定向,而绝大多数攻击脚本不会解析和执行这个逻辑,自然就被挡在外面了。

利用CDN的免费额度做前置过滤

很多人觉得CDN要花钱,其实国内多家CDN厂商都提供每月10GB到50GB不等的免费流量额度,对于个人博客、小型企业站完全够用。把域名解析到CDN上,源站只允许CDN的回源IP访问,这样所有请求先经过CDN节点。CDN厂商在边缘节点就做了基础的DDoS和CC防护,能过滤掉一部分明显的攻击流量。更重要的是,你可以利用CDN的IP白名单功能,只允许CDN的回源地址访问源站的80和443端口,其他所有IP直接丢弃。这样攻击者就算知道源站真实IP也打不进来,除非他通过CDN绕一圈,而CDN那层已经有基础防护了。配置方法是:在CDN控制台找到回源IP段列表,把这些IP段加到源站防火墙的白名单里,然后源站Nginx只监听内网IP或者只对CDN回源IP开放,其他来源一律返回444直接关闭连接。

动态页面静态化减少资源消耗

CC攻击最喜欢打的就是动态页面,因为每次请求都要查数据库、渲染模板,消耗资源是静态页面的几十上百倍。如果你的站点内容更新不频繁,完全可以把动态页面生成静态HTML文件,Nginx直接返回静态文件,连后端程序都不用启动。WordPress有大量缓存插件可以做这件事,非WordPress站点可以用wget写个定时脚本把页面抓取下来保存成HTML。更进一步,在Nginx配置里用try_files指令,先检查静态文件是否存在,存在就直接返回,不存在才转发给后端处理。这样即使攻击者打过来,Nginx返回静态文件的能力是极高的,一台1核1G的机器每秒都能处理几千个静态请求,攻击效果大打折扣。

日志监控和自动化封禁

防护策略部署好之后,还需要一个自动化的监控和响应机制。写一个shell脚本,每分钟分析一次Nginx访问日志,统计每个IP的请求次数,超过阈值的自动加入iptables黑名单。脚本逻辑不复杂:用awk处理日志,提取IP和请求时间,过滤出最近一分钟的请求,统计每个IP的出现次数,超过比如200次的就调用ipset add命令加入黑名单。把这个脚本加到crontab里每分钟执行一次。更进一步,可以分析状态码分布,如果某个IP返回了大量403或503,说明它触发了限流规则但仍然在持续请求,这种IP应该直接封禁更长时间。脚本示例:

#!/bin/bash
LOGFILE="/var/log/nginx/access.log"
THRESHOLD=200
tail -n 5000 $LOGFILE | awk -v d="$(date +%d/%b/%Y:%H:%M)" '$0 ~ d {print $1}' | sort | uniq -c | sort -nr | while read count ip; do
    if [ $count -gt $THRESHOLD ]; then
        ipset add blacklist $ip timeout 7200 2>/dev/null
    fi
done

这个脚本每次取最后5000行日志,匹配当前分钟的记录,统计IP出现次数,超过200次的封禁2小时。实际使用时要根据你的日志格式调整awk的字段位置和日期匹配方式。这套自动化封禁机制能让你在无人值守的情况下持续对抗攻击,攻击者换IP的速度再快,也快不过脚本每分钟一次的扫描和封禁。

多层防护的协同工作流程

把上面这些组件串起来,一个完整的请求处理流程是这样的:请求到达服务器,首先经过iptables,如果源IP在黑名单里直接丢弃,连TCP连接都不建立。通过iptables的请求到达Nginx,Nginx用Lua脚本检查UA和Referer等特征,不合格的直接返回403。合格请求再经过limit_req和limit_conn模块的频率和并发检查,超限的返回503。最后才到达后端应用,而后端应用前面还有静态文件缓存拦截。每一层都只处理自己能高效处理的部分,把绝大多数恶意请求挡在前面,真正到达后端应用的请求量已经很小了。这套方案的全部组件都是开源免费的,运行在一台最低配的云服务器上毫无压力,唯一需要投入的是你的时间去理解和配置。

实际部署中的细节和踩坑点

limit_req_zone定义时用的是$binary_remote_addr,这是二进制格式的IP地址,比直接用$remote_addr节省内存,对于IPv4地址只需要4个字节。zone名字后面跟的10m是共享内存大小,10MB大约能存16万个IP的状态信息,个人站点绰绰有余。burst参数设置时要小心,如果设得太大,攻击者可以利用这个窗口堆积请求;设得太小,正常用户在网络波动时可能会遇到误拦。建议根据实际业务调整,一般登录接口burst=2,普通页面burst=5到10。iptables的recent模块默认用内核内存记录IP信息,机器重启后会丢失,但ipset的黑名单可以配置成持久化存储,定期保存到文件,重启后恢复。另外要注意,如果你的站点前面有反向代理或者CDN,Nginx里拿到的$remote_addr是代理的IP而不是真实用户IP,需要在Nginx里配置real_ip_header从X-Forwarded-For或X-Real-IP头里获取真实IP,然后limit_req_zone里用$http_x_forwarded_for或者自定义变量,否则你限制的是CDN节点的IP,会把所有正常用户一起误拦。

整套方案跑起来之后,一台1核1G内存的轻量云服务器,在遭受每秒几百个请求的CC攻击时,CPU占用通常不会超过30%,网站访问基本不受影响。攻击规模再大的话,免费CDN那层会先扛一部分,到达源站的流量已经被削减过一轮。这套低成本防护体系的核心思想就是分层过滤、逐级削减、自动化响应,用最少的资源做最有效的拦截。个人站长不需要花几千上万去买商业防护产品,把开源工具组合用好,就能达到不错的效果。