网站CC攻击防御实战:利用Redis实现请求频率限制
摘要:# 网站CC攻击防御实战:用Redis给恶意请求“上锁” 我前两天刚处理一个客户的站,那场面,真叫一个惨烈。 一个做活动的小电商,上午十点流量一上来,服务器CPU直接飙到100%,页面卡得跟PPT似的。用户骂声一片,技术小哥急得满头汗。一查,不是什么…
网站CC攻击防御实战:用Redis给恶意请求“上锁”
我前两天刚处理一个客户的站,那场面,真叫一个惨烈。
一个做活动的小电商,上午十点流量一上来,服务器CPU直接飙到100%,页面卡得跟PPT似的。用户骂声一片,技术小哥急得满头汗。一查,不是什么复杂的DDoS,就是最典型的CC攻击——用一堆“肉鸡”,对着一个登录接口疯狂刷请求。说白了,就是“人海战术”,但用的是机器。
很多刚入行的朋友一听“CC攻击”就觉得头大,觉得得上高防、买WAF、搞一堆复杂策略。其实吧,很多所谓防护方案,PPT很猛,真被打的时候就露馅了。核心问题往往就一个:没拦住那些“不讲武德”的高频请求。
今天咱们不聊虚的,就聊一个最直接、最经济、也最有效的实战方案——用Redis实现请求频率限制。这招不敢说能防住所有攻击,但能帮你扛住90%以上的普通CC,而且成本极低,自己就能动手配。
一、CC攻击到底在“攻”什么?先别急着上方案
很多站长一发现网站卡,第一反应是“加带宽”、“升配置”。这感觉你懂吧?就像家里门被撞得咣咣响,你第一反应不是去堵门,而是想着把客厅装修得更结实——方向错了。
CC攻击(Challenge Collapsar)的核心逻辑很简单:用大量看似合法的HTTP请求,耗尽你的服务器资源。它不像DDoS那样拼流量,而是拼“连接数”和“处理能力”。
最常见的场景就是:
- 对着登录接口疯狂刷账号密码(撞库)
- 对着搜索接口疯狂发查询(拖慢数据库)
- 对着某个动态页面反复请求(吃光CPU)
这类攻击有个特点:单个IP的请求频率,远高于正常人类。
你想想,一个真实用户再快,1秒能点几次登录按钮?3次顶天了。但攻击脚本呢?一秒几十上百次跟玩儿似的。
所以,防御的第一道关卡,就是把这种“非人类”的请求速度给降下来。——这就是频率限制(Rate Limiting)要干的事。
二、为什么是Redis?因为“快”和“准”
说到频率限制,你可能听过用Nginx的limit_req模块,或者用程序内存计数。
这些方法行不行?行,但各有各的坑。
- Nginx限流:配置简单,但规则一旦复杂(比如要针对不同接口、不同用户做不同限制),写起来就头疼。而且它主要在网关层,对业务逻辑感知弱。
- 内存计数:比如用PHP的
$_SESSION或者Java的ConcurrentHashMap记数。问题是,你的服务如果是多台机器呢? 请求可能打到A服务器,也可能打到B服务器,内存计数各记各的,根本对不上。攻击者稍微换个IP或者轮询一下服务器,你这限制就形同虚设了。
这时候Redis的优势就出来了:
- 它是独立的、中心化的存储。不管用户请求打到哪台服务器,都去同一个Redis里查计数,公平统一。
- 它快得离谱。一次读写通常在毫秒级,几乎不会给正常请求增加明显延迟。
- 它自带过期时间。这是关键!你可以轻松实现“滑动窗口”计数,比如“每分钟最多60次”,时间一到自动清理,不用你写代码去扫。
说白了,Redis就像一个放在公区的秒表,谁跑得快、谁犯规了,它看得一清二楚。
三、实战代码:手把手给接口“上锁”
理论讲完,来点实在的。我以最常见的“用户登录接口”为例,展示一个Python(Flask框架)的实战代码。其他语言逻辑基本一样,你举一反三就行。
import redis
import time
from flask import Flask, request, jsonify
app = Flask(__name__)
# 连接Redis,这里用默认的本地6379端口,密码按实际情况填
r = redis.Redis(host='localhost', port=6379, password='', decode_responses=True)
def rate_limit(key, limit, window):
"""
key: 限流的标识,比如用户ID或IP
limit: 时间窗口内允许的最大请求数
window: 时间窗口,单位秒
"""
current = time.time()
# 用有序集合存储每次请求的时间戳
pipeline = r.pipeline()
pipeline.zremrangebyscore(key, 0, current - window) # 移除窗口外的旧记录
pipeline.zadd(key, {current: current}) # 添加本次请求
pipeline.zcard(key) # 获取当前窗口内的请求数
pipeline.expire(key, window + 1) # 设置key过期,避免内存泄漏
_, _, request_count, _ = pipeline.execute()
return request_count <= limit
@app.route('/login', methods=['POST'])
def login():
client_ip = request.remote_addr # 获取客户端IP
user_key = f"rate_limit:login:{client_ip}" # 用IP作为限流key
# 限制:1分钟内最多尝试10次登录
if not rate_limit(user_key, limit=10, window=60):
return jsonify({"error": "请求过于频繁,请1分钟后再试"}), 429 # HTTP 429 Too Many Requests
# 这里是正常的登录逻辑...
# username = request.form['username']
# password = request.form['password']
# ...
return jsonify({"status": "登录成功"})
if __name__ == '__main__':
app.run(debug=True)
这段代码干了啥?
- 每次用户请求登录,我们先拿他的IP构造一个Redis的key。
- 检查这个IP在过去60秒内,已经请求了多少次。
- 如果超过10次,直接返回429错误,请求根本不会走到后面的业务逻辑。
- 如果没超过,放行,并记录本次请求的时间戳。
为什么用有序集合(ZSET)?
因为它能方便地按时间戳范围删除旧记录(zremrangebyscore),实现“滑动窗口”。这是频率限制里最经典、也最准确的算法之一。
四、几个关键细节:别踩这些坑
方案看似简单,但真用起来,有几个地方特别容易出问题。我自己踩过坑,也看过不少站点栽在这里。
1. “误杀”正常用户怎么办?
比如公司出口IP是同一个,几十个员工同时登录,会不会被当成攻击封了?
会。所以千万别只用IP做唯一标识。
更稳妥的做法是:
- 如果用户已经登录,用
用户ID做key的一部分。 - 如果还没登录,但能拿到设备指纹或会话Cookie,也可以结合使用。
- 对于API接口,可以用
API Key来区分不同客户端的配额。
说白了,限流的粒度越细,误伤就越少。但这需要业务逻辑配合,是个权衡。
2. 攻击者换IP怎么办?
这是CC攻击的常见变招。坦白说,单靠频率限制防不住海量IP的低速攻击。
这时候需要组合拳:
- 前面加一层WAF或高防IP,识别并过滤掉明显的“肉鸡”IP段。
- 在业务层面,增加验证码(尤其是滑动拼图、点选等体验好的类型),在频繁请求后触发。
- 监控异常行为模式,比如同一个User-Agent在极短时间内从全球各地IP发起请求——这明显不是人。
3. Redis会不会成为瓶颈?
对于绝大多数中小网站,Redis处理这点计数请求,跟玩儿似的。
但如果你的QPS真的高到离谱(比如每秒几十万次),可以考虑:
- 使用Redis集群分片。
- 把计数逻辑放到更靠近客户端的CDN边缘(比如Cloudflare的Rate Limiting规则)。
- 在应用内存里做一层短期缓存,减少对Redis的访问。
不过说真的,真到了那个量级,你肯定已经有个专业的安全团队了,轮不到你看我这篇文章来操心。
五、最后说点大实话
安全防护这事儿,最怕两种心态:
一种是“裸奔到底,出事再说”;另一种是“堆砌方案,越复杂越好”。
其实吧,有效的防护,往往是简单、直接、打在七寸上的。
用Redis做频率限制,就是这样一个“七寸”方案。它不贵(甚至免费,如果你已经有Redis),不难(代码就几十行),不拖累性能(正常用户毫无感知)。但它能实实在在地,把那些无脑刷接口的脚本挡在门外。
当然,它不是银弹。真正的安全是一个体系,从网络层到应用层,从预防到监控到应急响应。
但如果你现在源站还裸奔,或者只靠一个“低配高防”硬撑,我劝你今晚就把这套代码加上。花不了半小时,但可能在你下次被刷的时候,救你一命。
行了,不废话了,搞代码去吧。
有具体问题,评论区见——虽然我也不敢保证秒回,但看到的都会尽量答。

