各位朋友,大家好!
我是你们的老朋友,一个每天盯着服务器日志,像盯着菜市场大妈抢特价鸡蛋一样盯着流量的人。今天咱们不讲别的,咱们专门来聊聊那个让人闻风丧胆、让人半夜惊醒、让运维直呼“亲娘咧”的东西——恶意爬虫。
你要知道,在这个世界上,好人虽然多,但捣乱的人更多。特别是那些写脚本的“脚本小子”,他们手里拿着几十个线程,拿着Python或者Java,像一群闻着血腥味的鲨鱼一样围攻你的服务器。你的CPU瞬间飙红,数据库CPU爆表,最后你的网站打开像是在“渡劫”,而不是在服务客户。
很多新手程序员,遇到这种情况的第一反应是:“重启一下服务器?”错了!那是掩耳盗铃!咱们今天就要从PHP的角度,以讲座的形式,手把手教你如何构建一个铜墙铁壁。我们要教这些爬虫明白一个道理:这里不是它们的后花园,这是我的地盘!
准备好了吗?咱们把“披萨”拿开,把代码敲起来。
第一章:给每个爬虫“验明正身”——User-Agent深度解析
你想抓我的数据?行啊,先看看你长什么样。
每一个HTTP请求,哪怕是你的浏览器发起的,都会带上一个“身份证”,叫 User-Agent。这个字符串就像是你出门穿的西装革履。对于正常用户,西装革履代表Chrome或者Edge;但对于爬虫,它通常赤身裸体,或者穿着一身写着“我是机器人”的睡衣。
我们可以通过 $_SERVER['HTTP_USER_AGENT'] 来读取这个信息。
第一招:正则匹配的“火眼金睛”
别傻傻地去 if ($agent == 'Mozilla...'),那是给死人看的。我们要用正则表达式。Python爬虫最喜欢伪装成正常的浏览器,但这事儿很难,因为它们很容易露出马脚。
比如,著名的 curl 和 wget,它们非常诚实,User-Agent里直接写着 curl/7.68.0 或者 Wget/1.20.3。如果你看到这些字眼,你可以先放行,但加上标记。
再比如,那些疯狂的扫描脚本,它们User-Agent里常带有 /, +, - 这种连字符,这是机器生成的特征。
来看这段代码,这是咱们防御的“哨兵”:
<?php
// 在任何请求的最开始(包括index.php的顶部)
function checkUserAgent() {
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
// 1. 恶意扫描器特征
// 通常是 'curl', 'wget', 'python', 'java' 等混杂着各种乱码
// 这里的正则大意是:如果包含 curl, wget, python, java, bingo等,且包含奇怪的符号
if (preg_match('/curl|wget|python|java|bingo/i', $ua)) {
// 你可能想记录日志
error_log("Blocked suspicious UA: " . $ua);
// 直接封杀,别废话
die("Go away, you bot!");
}
// 2. 伪造浏览器的常见特征
// 比如 "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
// 这种虽然名字叫Googlebot,但是它访问你的后台接口,那就是伪装
if (preg_match('/compatible|bot|spider|crawler/i', $ua)) {
// 如果你希望Googlebot进来,得加个白名单判断
// 这里做个简单的演示
if (strpos($ua, 'Googlebot') === false && strpos($ua, 'Baiduspider') === false) {
die("Access Denied: Suspicious Bot Detected.");
}
}
// 3. 处理那些连User-Agent都没有的“裸奔”请求
// 有些老旧的爬虫或者脚本,为了省流量,根本不带UA
if (empty($ua)) {
header('HTTP/1.1 403 Forbidden');
echo "Please provide a User-Agent header.";
exit;
}
}
// 调用这个“哨兵”
checkUserAgent();
?>
这段代码虽然简单,但它能挡住80%的脚本小子。如果User-Agent里全是乱码,或者全是Mozilla/5.0 (Windows NT 10.0; Win64; x64)这种标准格式,那就说明可能是个伪装得很好的爬虫,这时候我们需要更高级的手段。
第二章:频率控制——别逼我给你“一拳”
有时候,User-Agent很难伪装。这帮人实在太过分了,他们甚至懒得改名字,直接拿你的网站开刀。你刚才访问了一下首页,过一秒又访问了一下,再过一秒又访问了一下。这就是典型的“高频攻击”。
这时候我们需要引入频率控制。
2.1 基础版:基于文件的“黑名单库”
在高端一点的Redis出来之前,老一辈的程序员喜欢用文件锁来控制。虽然性能不如Redis,但原理是一样的:记下谁在什么时候访问了,访问了多少次。
<?php
function checkRateLimit($ip) {
// 定义日志文件
$logFile = 'rate_limit.log';
$maxRequests = 10; // 允许的请求次数
$window = 60; // 时间窗口(秒)
// 1. 如果文件不存在,或者旧数据过期,清空重置
// 这里为了演示简单,每次请求都重写文件。
// 生产环境建议用Redis,或者更高效的文件读写方式
if (!file_exists($logFile)) {
file_put_contents($logFile, json_encode([]));
}
$logs = json_decode(file_get_contents($logFile), true);
// 清理过期的日志
$now = time();
foreach ($logs as $key => $val) {
if ($now - $val['time'] > $window) {
unset($logs[$key]);
}
}
// 2. 检查当前IP的请求次数
$count = 0;
foreach ($logs as $log) {
if ($log['ip'] === $ip) {
$count++;
}
}
if ($count >= $maxRequests) {
// 触发惩罚
header('HTTP/1.1 429 Too Many Requests');
header('Retry-After: 60');
die("Too many requests. Chill out, buddy.");
}
// 3. 记录这次请求
$logs[] = ['ip' => $ip, 'time' => $now];
file_put_contents($logFile, json_encode($logs));
}
?>
但是,兄弟们,这个代码有个巨大的Bug。如果你在两个请求之间,PHP进程被切走了,或者文件写入失败,那计数器就丢了。而且,并发写入文件会导致死锁。
所以,为了不让大家写这种垃圾代码,我们直接上Redis。这是PHP爬虫防御的终极武器。
2.2 终极版:Redis 滑动窗口算法
Redis的INCR命令是原子性的,这意味着即使有1000个请求同时进来,Redis也能保证计数准确无误。我们不仅要计数,还要控制时间窗口。
<?php
// 假设你已经连接了Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
function blockByRedis($ip, $redis) {
// 定义限制
$limit = 60; // 60秒内允许60次请求
$key = "rate_limit:ip:" . $ip;
// 获取当前时间戳
$now = time();
// 1. 模拟滑动窗口
// 我们不直接存总数,而是存过去60秒内每个秒数的请求计数
// 比如 key: rate_limit:ip:127.0.0.1:123456 (代表13:14:56这个秒数的请求)
// 先移除60秒之前的数据
$redis->zRemRangeByScore($key, 0, $now - $limit);
// 2. 增加当前秒数的计数
$currentSecondCount = $redis->zIncrBy($key, 1, $now);
// 3. 检查总数
if ($currentSecondCount > $limit) {
// 阻止访问
die("You are too fast! Speed limit exceeded.");
}
// 4. 设置过期时间,防止内存泄漏
$redis->expire($key, $limit + 10);
}
// 在你的业务逻辑入口处调用
blockByRedis($_SERVER['REMOTE_ADDR'], $redis);
?>
这段代码非常优雅。它使用了一个有序集合 ZSET,每个成员的分数是时间戳,成员值是1。每次请求进来,zIncrBy 加1。然后我们通过 zRemRangeByScore 删掉60秒以前的记录。如果60秒内所有的分数加起来超过60,那就说明你疯了。
第三章:浏览器指纹识别——JavaScript 抓捕令
有时候,爬虫太聪明了,它们能完美伪造User-Agent,甚至能通过基本的频率限制。它们会假装成Safari浏览器,甚至去下载你的JS文件执行。
这时候,我们需要一个只有真人浏览器才能完成的任务。我们可以在HTML里藏一个“暗号”,只有你的浏览器能看见,但爬虫看不见。
3.1 前端:生成“暗号”
在页面加载的时候,我们生成一个随机字符串,存入一个Cookie或者隐藏的input里。注意,这个Cookie一定要设置 HttpOnly,这样JS读不到,防止XSS窃取(虽然这里主要用于防爬虫)。
<!DOCTYPE html>
<html>
<head>
<title>我的超级网站</title>
</head>
<body>
<h1>欢迎光临</h1>
<!-- 这是一个隐藏的输入框,里面藏着只有真实浏览器才能看到的Token -->
<input type="hidden" name="csrf_token" id="hidden_token" value="">
<script>
// 页面加载时,把这个Token设置到Cookie里
const token = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
document.cookie = "auth_token=" + token + "; path=/";
// 把它填到那个隐藏框里
document.getElementById('hidden_token').value = token;
</script>
<form action="/api/login" method="POST">
<input type="text" name="username">
<input type="password" name="password">
<button type="submit">提交</button>
</form>
</body>
</html>
3.2 后端:验证“暗号”
如果用户直接访问这个页面,没有执行上面的JS脚本,那么Cookie里就没有 auth_token。或者用户提交表单时,没有带这个隐藏框的值。这时候,我们就可以判定它是爬虫。
<?php
function verifyRealBrowser() {
$tokenFromCookie = isset($_COOKIE['auth_token']) ? $_COOKIE['auth_token'] : '';
$tokenFromInput = isset($_POST['csrf_token']) ? $_POST['csrf_token'] : '';
// 只有当Cookie里有,且表单里有,并且两者一致,才是合法的人类
if (empty($tokenFromCookie) || empty($tokenFromInput) || $tokenFromCookie !== $tokenFromInput) {
// 惩罚:立即封禁
die("Access Denied: You are not a human browser.");
}
// 验证通过
return true;
}
// 在任何业务逻辑前调用
verifyRealBrowser();
?>
这一招非常狠。爬虫虽然能模拟HTTP请求,但它们很难完美模拟JavaScript的执行。如果爬虫通过你主页抓取数据,发现数据不对,因为JS没执行,它就会认为你的页面出错了,从而放弃或者报错。这是对爬虫最直接的降维打击。
第四章:验证码——智商的试金石
如果说前面几招是“安检门”,那验证码就是“测谎仪”。面对疯狂的攻击,没有什么是一张乱码图片解决不了的。如果有,那就两张。
4.1 基础版:使用 captcha 库(推荐)
不要自己写验证码生成算法,你会写出bug百出的代码。直接用开源库,比如 mews/captcha。这东西能生成带干扰线、扭曲文字的验证码,识别率极高。
<?php
// 安装 composer require mews/captcha
use MewsCaptchaCaptcha;
$captcha = new Captcha();
// 显示验证码图片
// $captcha->create('math') 或者 'recaptcha'
// 然后把这个生成的图片输出到浏览器
// 注意:实际使用时,你需要配置 captcha 的配置文件,指定字体路径、宽度高度等
if (request()->has('captcha')) {
if ($captcha->check(request()->input('captcha'))) {
// 验证成功
} else {
// 验证失败
die("Captcha Wrong! Try again.");
}
}
?>
4.2 高级版:无感验证(BotDetect 风格)
如果你不想让用户体验变差(不想每点一下都要输验证码),你可以做一个“可疑检测”。当系统检测到一个IP在1秒内点击了10次,或者User-Agent是机器人时,直接弹出验证码,而不是直接封杀。
<?php
if (isSuspiciousActivity($ip)) {
// 生成验证码
$captchaCode = generateCaptcha();
$_SESSION['captcha'] = $captchaCode;
// 返回HTML,包含图片和输入框
echo "<img src='captcha_image.php'>";
echo "<input type='text' name='captcha'>";
// 注意:这里通常需要配合前端JS,判断输入正确后才能继续操作
exit;
}
function isSuspiciousActivity($ip) {
// 逻辑:如果当前IP在最近5分钟内,错误次数超过3次,或者是机器人UA
// 这里省略具体的Redis计数逻辑,复用第二章的思路
return false;
}
?>
第五章:IP信誉系统——寻找你的“老熟人”
有时候,我们不能光靠规则。我们需要一个“黑名单”。很多IP段是专门干坏事儿的,比如俄罗斯、巴西的某些IP段,或者是已知的恶意IP数据库。
5.1 本地黑名单文件
在PHP中维护一个巨大的文本文件,里面全是IP。每次请求都读取这个文件。
function checkIpBlacklist($ip) {
$blacklistFile = 'ip_blacklist.txt';
$blacklist = file($blacklistFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($blacklist as $blockedIp) {
if (strpos($ip, $blockedIp) !== false) {
// 这里用 strpos 而不是 equals,是因为可以匹配IP段
// 比如 192.168.1.* -> 192.168.1.1 就匹配上了
die("IP is blocked!");
}
}
}
5.2 开源API联动
这招比较狠。你可以写一个PHP函数,在每次请求前调用第三方API(比如 AbuseIPDB 或 IPQualityScore)。
function checkIpReputation($ip) {
// 假设你有API Key
$apiKey = 'your_api_key';
$url = "https://api.abuseipdb.com/api/v2/check?ipAddress={$ip}&maxAgeInDays=90";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Key: ' . $apiKey]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
// 如果信誉分低于10,直接拉黑
if (isset($data['data']['abuseConfidenceScore']) && $data['data']['abuseConfidenceScore'] > 90) {
// 写入你的Redis黑名单
die("IP Reputation too low. Blocked.");
}
}
这一招能帮你挡住90%的僵尸网络攻击。
第六章:综合防御演练——搭建“护城河”
现在我们有了独立的招式。但是,一个完整的PHP应用,需要把这些招式串联起来。这就好比建房子,地基(服务器配置)、围墙(Nginx/WAF)、铁门(PHP逻辑)、防盗报警器(验证码)缺一不可。
6.1 Nginx 层面的拦截(前置防线)
在PHP执行之前,Nginx就能干掉很多事。这是最快的。
# Nginx 配置示例
server {
# 1. 拦截常见的扫描脚本 User-Agent
if ($http_user_agent ~* (curl|wget|python|java)) {
return 403;
}
# 2. 拦截全黑的 User-Agent
if ($http_user_agent = "") {
return 403;
}
# 3. 针对特定IP段的封禁(比如来自某个已知垃圾邮件服务器的IP)
# if ($http_x_forwarded_for ~* "123.123.123.123") {
# return 403;
# }
location ~ .php$ {
# 转发给PHP-FPM
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
6.2 PHP 应用层防御(核心代码)
这是我们要写的主要逻辑。我会整合User-Agent检测、Redis频率限制和IP信誉检查。
<?php
/**
* 终极防爬虫控制器
*/
// 1. 基础配置
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$ip = $_SERVER['REMOTE_ADDR'];
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
// 2. Nginx 层没拦住的,PHP 层再拦
// 检查黑名单
if (isIpBlacklisted($ip, $redis)) {
die("403 Forbidden: IP Blocked");
}
// 检查 User-Agent
if (isSuspiciousUA($ua)) {
logAttack($ip, $ua, 'Suspicious UA');
die("403 Forbidden: Bad Robot");
}
// 3. 频率限制
$rateLimitKey = "rate_limit:" . $ip;
if (checkRateLimit($rateLimitKey, $redis, 30, 60)) { // 30次/分钟
die("429 Too Many Requests: Chill out.");
}
// 4. 逻辑处理...
echo "Welcome, legitimate human user!";
?>
为了方便大家直接复制运行,我把辅助函数也写出来:
function isIpBlacklisted($ip, $redis) {
// 从 Redis 集合中检查
return $redis->sIsMember("blacklist", $ip);
}
function isSuspiciousUA($ua) {
// 匹配常见的恶意特征
return preg_match('/(curl|wget|python|java|bot|spider|scraper|tracker|crawler)/i', $ua);
}
function logAttack($ip, $ua, $reason) {
// 记录到日志文件
$log = date('Y-m-d H:i:s') . " | IP: {$ip} | UA: {$ua} | Reason: {$reason}n";
file_put_contents('attack_log.txt', $log, FILE_APPEND);
}
function checkRateLimit($key, $redis, $maxRequests, $windowSeconds) {
$now = time();
$redis->zRemRangeByScore($key, 0, $now - $windowSeconds);
$count = $redis->zIncrBy($key, 1, $now);
$redis->expire($key, $windowSeconds);
return $count > $maxRequests;
}
第七章:应对高级对抗——加密与混淆
爬虫变得越来越强,我们也不能坐以待毙。我们要学会“隐身”。
7.1 加密请求参数
不要直接在URL里传参数。比如 /api/get_data?page=1。爬虫看到这个URL,就知道你的分页逻辑,疯狂遍历1, 2, 3…。
你可以把这些参数加密,或者加上时间戳。
// 前端 JS: 生成签名
// url: /api/data?token=abc123&t=123456789
// 后端 PHP: 验证 token 和 t 的时间差是否在5秒内,且 token 是否有效
7.2 混淆输出
在HTML输出时,不要直接输出纯文本。使用JSONP?不,那太老土了。使用JSONP或者构建特殊的JSON结构。如果爬虫直接正则匹配 {"data": "..."},可能会匹配失败,因为它可能被加了引号、空格,或者结构是嵌套的。
但这个方法只能防住低级的脚本,对专业的爬虫没什么用。
结语(其实是实战建议)
朋友们,讲了这么多,其实核心思想就一句话:不要信任任何人,包括浏览器。
- 分层防御:Nginx 拦IP,PHP 拦逻辑。
- 多管齐下:UA检查 + 频率限制 + JS挑战。
- 数据持久化:别用文件存计数器,用 Redis。
- 不断迭代:爬虫在变,你的规则也要变。每天看看
attack_log.txt,分析你的访客是谁。
如果你的服务器现在还是裸奔状态,我建议你立刻按照我上面的代码,把 checkUserAgent 和 checkRateLimit 塞进你的代码里。相信我,这一两行代码,能给你省下不少服务器费用,也能让你少睡几个觉。
好了,今天的讲座就到这里。如果你们觉得这些招式有用,记得转发给你那还在被DDoS搞崩溃的老板看。咱们下回再见,记得锁好门!