本文目录导读:

- 基础防线:来源与身份校验
- 核心防线:唯一用户认证(业务层)
- 行为与频率控制:触发式防御
- 技术层:前后端防御(提高攻击门槛)
- 数据库与写入层:绝对防重复
- 综合策略建议(从低到高成本,根据项目重要性选用)
- 代码示例(PHP + Redis + MySQL)
为PHP项目的投票功能设计防刷机制,需要从前端、后端、业务逻辑三个层面综合防御,没有100%绝对安全的系统,但通过多层叠加可以极大提高攻击成本。
以下是防刷防重复的体系化方案:
基础防线:来源与身份校验
这是最基础的,但容易被忽视。
- IP限制:记录用户IP,对同一IP在单位时间(如1小时)内的投票次数进行限制。
- 缺点:用户可通过代理/VPN更换IP。
- Cookie/本地存储验证:用户投票后,在浏览器写入Cookie或LocalStorage标记。
- 缺点:用户可清除本地数据。
- Session/Token校验:用户登录后,同一Session或Token在有效期内只能投一票。
- 缺点:未登录用户无法限制(可改为强制登录)。
核心防线:唯一用户认证(业务层)
这是防刷的关键,需要根据业务场景选择合适的唯一标识。
- 强制用户登录:要求用户必须注册并登录后才能投票,后台关联
user_id与投票记录。- 优点:识别度高,可追溯。
- 缺点:降低用户体验。
- 手机号验证:投票前需短信验证,每个手机号只能投一次。
- 适用范围:重要投票(如选秀、评优)。
- 设备指纹识别:通过JavaScript收集用户设备的特征(浏览器、分辨率、字体、显卡、Canvas指纹等),生成唯一的设备ID。
- 优点:即使用户换IP、清Cookie,设备指纹也较难改变。
- 推荐类库:
FingerprintJS。
- 第三方账号绑定:强制使用微信、QQ、微博等第三方登录,利用平台的唯一标识。
行为与频率控制:触发式防御
- 单位时间频率限制(Rate Limit):
- 使用Redis等中间件控制同一个IP、同一个用户ID在每分钟/每小时/每天的投票次数。
- 示例:同一IP每分钟最多投10次,每天最多投100次。
- 投票间隔限制:两次投票之间必须有一定的时间间隔(如至少3秒)。
- 日志与异常行为分析:
- 记录投票时间、IP、User-Agent、Cookie、设备指纹。
- 告警:短时间内投票数激增、同一个User-Agent下出现数百个不同IP、投票时间间隔完全一致(机器刷票特征)。
技术层:前后端防御(提高攻击门槛)
- 验证码:
- 滑块验证码/图形验证码:每次投票都需要拖动或点击。
- 智能验证码(如极验、腾讯云验证码):基于行为轨迹判断是否为真人,对用户干扰较小。
- 注意:仅第一次或异常时弹出,不要每次弹出影响体验。
- Token防重放:
- 每次返回投票页面时,生成一个一次性
nonce(随机数)绑定到提交表单中,服务器端校验该nonce是否已被使用。 - 示例:
// 生成 $_SESSION['vote_token'] = bin2hex(random_bytes(32)); // 校验 if (hash_equals($_SESSION['vote_token'], $_POST['token'])) { unset($_SESSION['vote_token']); // 立即销毁 // 处理投票 }
- 每次返回投票页面时,生成一个一次性
- 请求签名验证:前端将投票参数(
item_id、user_id、timestamp等)加上密钥进行hash签名,后端验证签名是否合法且未过期(可防止恶意构造请求)。
数据库与写入层:绝对防重复
这是最后一道屏障,利用数据库的唯一约束来保证同一个人无法投两次同一选项。
- 联合唯一索引:
- 创建数据表时,设置
(user_id, option_id)或(ip, option_id, date)为联合唯一索引。 - 写入时使用
INSERT ... ON DUPLICATE KEY UPDATE或INSERT IGNORE,如果插入失败,则表示已投过。
- 创建数据表时,设置
- 悲观/乐观锁:在高并发场景下,防止投票计数超卖。
UPDATE vote_options SET count = count + 1 WHERE id = ? AND count < max_vote_count;
综合策略建议(从低到高成本,根据项目重要性选用)
| 场景 | 推荐方案 |
|---|---|
| 简单活动/内部投票 | IP限制 + Cookie + 简单图形验证码 |
| 常规社区投票 | 强制登录(Session) + 单位时间频率限制 + Token防重放 |
| 重要商业投票 | 手机号验证/设备指纹 + 滑块验证码 + 联合唯一索引 + 业务日志分析 |
| 高价值/严肃投票 | 所有以上方案 + 人工审核 + 投票前提交身份证后六位 + 区块链存证 |
代码示例(PHP + Redis + MySQL)
<?php
session_start();
// 1. 基础验证:是否已登录
if (!isset($_SESSION['user_id'])) {
die("请先登录");
}
$userId = $_SESSION['user_id'];
$optionId = $_POST['option_id'] ?? 0;
$voteToken = $_POST['vote_token'] ?? '';
// 2. Token防重放 (session方式)
if (!hash_equals($_SESSION['vote_token'], $voteToken)) {
die("请求非法或已过期");
}
unset($_SESSION['vote_token']); // 立即销毁
// 3. 频率限制 (Redis)
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$rateKey = "vote:rate:" . $userId . ":" . date('YmdH'); // 按小时限制
if ($redis->incr($rateKey) > 10) { // 每小时最多投10票
die("您的投票次数已达上限");
}
$redis->expire($rateKey, 3600);
// 4. 时间间隔检测 (Redis)
$intervalKey = "vote:interval:" . $userId;
if ($redis->exists($intervalKey)) {
$lastTime = $redis->get($intervalKey);
if (time() - $lastTime < 5) { // 至少5秒间隔
die("请勿频繁操作");
}
}
$redis->set($intervalKey, time());
// 5. 写库 (利用联合唯一索引)
try {
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$stmt = $pdo->prepare("INSERT INTO vote_log (user_id, option_id, created_at) VALUES (?, ?, NOW())");
$stmt->execute([$userId, $optionId]);
// 更新统计
$pdo->exec("UPDATE vote_options SET votes = votes + 1 WHERE id = " . intval($optionId));
echo "投票成功";
} catch (PDOException $e) {
if ($e->getCode() == 23000) { // 重复插入错误
die("您已经投过票了");
}
die("系统错误");
}
防刷没有银弹,建议分层防御,即使破解了Cookie(第一层),还有IP限制(第二层)、设备指纹(第三层)、验证码(第四层)、数据库唯一索引(最后一层),对于商业项目,强制手机号验证 + 设备指纹 + 频率限制是目前性价比较高的组合。