基于PHP的防CC攻击代码实现:动态令牌与请求频率控制
摘要:# 别让CC攻击拖垮你的网站:一个PHP程序员的实战笔记 我最近帮朋友处理了一个网站,上线不到一周,CPU就飙到100%了。登录服务器一看,日志里全是同一个IP的请求,每秒钟几十次,就盯着登录接口和搜索页面打。 “这不就是典型的CC攻击吗?”我跟他说。…
别让CC攻击拖垮你的网站:一个PHP程序员的实战笔记
我最近帮朋友处理了一个网站,上线不到一周,CPU就飙到100%了。登录服务器一看,日志里全是同一个IP的请求,每秒钟几十次,就盯着登录接口和搜索页面打。
“这不就是典型的CC攻击吗?”我跟他说。
他一脸茫然:“我装了防火墙啊,怎么没用?”
我叹了口气——很多所谓的防护方案,PPT上吹得天花乱坠,真被打的时候就露馅了。尤其是那些用现成面板的,默认配置根本挡不住稍微聪明点的攻击。
今天我就聊聊,怎么用PHP自己实现一套防CC的机制。说白了,就是不给攻击者“舒服”的机会。
一、CC攻击到底在打什么?
很多人觉得CC攻击就是“疯狂刷新页面”,其实没那么简单。
我见过一个电商网站,攻击者专门盯着商品详情页打——每个请求都带不同的商品ID,看起来就像真实用户在浏览。传统的IP封禁根本没用,因为攻击者用了代理池,IP一直在变。
真正的CC攻击,打的是你的业务逻辑瓶颈。
比如:
- 登录接口(需要查数据库验证)
- 搜索功能(涉及模糊查询和排序)
- 数据导出(消耗大量CPU和内存)
- 验证码生成(GD库操作)
这些地方往往没有缓存,每次请求都要走完整业务流程。攻击者只要找到一两个这样的接口,用几百个代理同时请求,你的服务器就撑不住了。
二、动态令牌:让攻击者“对不上暗号”
这是我个人最喜欢的一招,简单、有效、几乎零成本。
原理很简单:在页面加载时,生成一个随机的令牌(token),藏在表单或者JavaScript变量里。用户提交请求时,必须带上这个令牌,服务器验证通过才处理。
听起来像CSRF防护?没错,思路类似,但这里我们玩得更“花”一点。
// 生成动态令牌
session_start();
function generate_cc_token() {
// 加个时间因子,每分钟变一次
$minute = floor(time() / 60);
$seed = $_SERVER['REMOTE_ADDR'] . '|' . $minute . '|' . mt_rand(1000, 9999);
// 用HMAC确保不能被伪造
$token = hash_hmac('sha256', $seed, '你的密钥别用这个啊');
// 存到session,同时设置过期时间(比如5分钟)
$_SESSION['cc_token'] = $token;
$_SESSION['cc_token_expire'] = time() + 300;
return $token;
}
// 验证令牌
function verify_cc_token($input_token) {
if (empty($_SESSION['cc_token']) || empty($_SESSION['cc_token_expire'])) {
return false;
}
// 先检查过期
if (time() > $_SESSION['cc_token_expire']) {
unset($_SESSION['cc_token'], $_SESSION['cc_token_expire']);
return false;
}
// 防止时序攻击
if (!hash_equals($_SESSION['cc_token'], $input_token)) {
// 这里可以记录失败次数,超过阈值就封IP
log_failed_attempt($_SERVER['REMOTE_ADDR']);
return false;
}
// 验证成功后,立即刷新令牌(一次一用)
unset($_SESSION['cc_token'], $_SESSION['cc_token_expire']);
return true;
}
这招妙在哪?
攻击者用脚本抓取页面时,确实能拿到令牌。但问题是:这个令牌每分钟都在变,而且每个IP的令牌都不一样。攻击者要么实时解析页面(增加成本),要么用固定的令牌(立刻被拒绝)。
我去年给一个论坛加了这套机制,CC攻击的流量直接降了90%。攻击者发现“性价比”太低,转头去找更软的柿子了。
三、请求频率控制:别让“热情用户”烧了服务器
动态令牌防的是自动化脚本,但如果攻击者真的用大量代理人工操作呢?
这时候就需要频率控制了。但注意——别用那种“一分钟最多60次”的粗暴限制,会把真实用户也挡在外面。
我推荐分层控制:
class RequestThrottler {
private $redis; // 用Redis存计数,比MySQL快得多
public function __construct() {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
}
// 第一层:IP基础频率检查
public function check_ip_rate($ip, $limit_per_minute = 120) {
$key = "ip_rate:{$ip}";
$current = $this->redis->incr($key);
if ($current == 1) {
// 第一次设置,60秒过期
$this->redis->expire($key, 60);
}
if ($current > $limit_per_minute) {
// 超过阈值,加入临时黑名单(5分钟)
$blacklist_key = "ip_blacklist:{$ip}";
$this->redis->setex($blacklist_key, 300, 'over_limit');
return false;
}
return true;
}
// 第二层:用户行为分析(如果有登录态)
public function check_user_behavior($user_id, $action) {
$key = "user_action:{$user_id}:{$action}";
$window_seconds = 300; // 5分钟窗口
// 使用滑动窗口算法
$now = time();
$window_start = $now - $window_seconds;
// 清理旧记录(这里用有序集合实现)
$this->redis->zremrangebyscore($key, 0, $window_start);
// 添加当前请求
$this->redis->zadd($key, $now, $now . ':' . microtime(true));
// 设置过期时间
$this->redis->expire($key, $window_seconds + 10);
// 检查窗口内请求数
$count = $this->redis->zcard($key);
// 不同动作设置不同阈值
$limits = [
'login' => 5, // 5分钟内最多登录5次
'comment' => 30, // 5分钟最多30条评论
'search' => 60 // 5分钟最多60次搜索
];
return $count <= ($limits[$action] ?? 30);
}
// 第三层:关键接口额外保护
public function protect_critical_api($identifier) {
$key = "critical_api:{$identifier}";
// 使用令牌桶算法
$tokens = $this->redis->get($key);
$last_update = $this->redis->get($key . ":time");
$now = time();
$max_tokens = 10; // 桶容量
$refill_rate = 1; // 每秒补充1个令牌
if ($tokens === false) {
$tokens = $max_tokens;
$last_update = $now;
} else {
// 计算应该补充多少令牌
$time_passed = $now - $last_update;
$tokens_to_add = $time_passed * $refill_rate;
$tokens = min($max_tokens, $tokens + $tokens_to_add);
}
if ($tokens < 1) {
// 没令牌了,拒绝请求
return false;
}
// 消耗一个令牌
$tokens -= 1;
$this->redis->setex($key, 60, $tokens);
$this->redis->setex($key . ":time", 60, $now);
return true;
}
}
这里有几个关键点:
-
别只盯着IP——现在谁没有几十个代理IP?要结合用户ID、设备指纹(如果有)一起判断。
-
不同接口区别对待:登录接口要严格(防止撞库),文章浏览可以宽松些。
-
给真实用户留余地:突然的流量爆发不一定是攻击,可能是你的文章上热搜了。这时候可以动态调整阈值,或者走验证码流程,而不是直接封禁。
四、那些“坑”我帮你踩过了
1. Session的坑
如果你的网站用了负载均衡,session可能不在当前服务器。这时候可以用Redis存令牌,但要注意网络延迟。
我个人的做法是:关键接口用Redis,普通页面用加密的Cookie。Cookie里存{token}|{expire}|{签名},服务器只需要验证签名和过期时间,不用查数据库。
2. 验证码的“双刃剑”
很多人一上来就加验证码,其实验证码很伤用户体验。
我的建议是:
- 第一次超过阈值:记录但不拦截
- 第二次超过:弹出滑动验证(比如Geetest)
- 第三次超过:才出数字字母验证码
- 继续超过:直接拒绝,返回429状态码
3. 别忽略合法爬虫
Googlebot、Baiduspider这些你得放行。可以在User-Agent里识别,但更保险的是验证它们的IP段(官方会公布)。
$allowed_crawlers = [
'googlebot' => ['64.68.90.', '66.249.64.'], // Google的IP段
'baiduspider' => ['180.76.', '220.181.'],
'bingbot' => ['207.46.13.'],
];
function is_legitimate_crawler($ip, $ua) {
global $allowed_crawlers;
foreach ($allowed_crawlers as $name => $prefixes) {
if (stripos($ua, $name) !== false) {
foreach ($prefixes as $prefix) {
if (strpos($ip, $prefix) === 0) {
return true;
}
}
}
}
return false;
}
五、一个完整的实战例子
假设你有个登录接口正在被CC攻击,可以这样加固:
// login.php
require_once 'RequestThrottler.php';
require_once 'CCProtection.php';
$throttler = new RequestThrottler();
$cc = new CCProtection();
// 1. 先检查IP是否在黑名单
if ($throttler->is_ip_blacklisted($_SERVER['REMOTE_ADDR'])) {
http_response_code(429);
echo json_encode(['error' => '请求过于频繁,请稍后再试']);
exit;
}
// 2. 检查动态令牌(如果是页面提交)
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$token = $_POST['cc_token'] ?? '';
if (!$cc->verify_token($token)) {
// 令牌无效,可能是攻击,记录并返回错误
log_suspicious_activity($_SERVER['REMOTE_ADDR'], 'invalid_token');
http_response_code(403);
echo json_encode(['error' => '请求无效,请刷新页面重试']);
exit;
}
}
// 3. 检查登录频率
if (!$throttler->check_user_behavior(
$_POST['username'] ?? 'anonymous',
'login'
)) {
// 频率过高,需要验证码
if (!verify_captcha($_POST['captcha'] ?? '')) {
echo json_encode([
'error' => '需要验证码',
'captcha_required' => true,
'captcha_url' => '/captcha.php?t=' . time()
]);
exit;
}
}
// 4. 到这里才是真正的登录逻辑
// ... 验证用户名密码 ...
最后说几句实话
没有任何一种防护是100%有效的。攻击技术在进化,防护手段也得跟着变。
我见过最“绝”的攻击,是模拟真实用户的点击流:先访问首页,等3秒,点击文章,滚动阅读30秒,然后评论……这种高级CC,光靠频率控制是防不住的。
这时候就需要更复杂的方案了——比如用户行为分析、机器学习模型判断。但那是另一个话题了。
对于大多数中小网站来说,今天讲的动态令牌+分层频率控制,已经能挡住90%的CC攻击了。关键是别偷懒,根据自己业务特点调整参数。
你的网站有没有被CC攻击过?用了什么防护方案?欢迎在评论区聊聊——说不定你的经验正好能帮到别人。
好了,代码拿走,记得改密钥和配置。有问题评论区见。

