防刷票的核心在于“层层设防”,单一的手段很容易被绕过。一个成熟的防御体系必须包含网关流量清洗、应用层精准限流、请求合法性校验以及数据库层面的最终兜底。本文将基于Linux环境,使用Nginx、Redis、Node.js和MySQL构建一套完整的防御方案,确保每一步都能直接落地。
在请求到达业务代码之前,利用Nginx的高性能特性进行IP级别的流量清洗。这是最廉价且最有效的手段,能够直接拦截绝大多数低成本的脚本攻击。
确保系统已安装Nginx,执行以下命令进行安装:
sudo apt-get update
sudo apt-get install nginx
编辑Nginx配置文件 /etc/nginx/nginx.conf,在 http 块中添加限流区域定义,并在 server 块中应用规则:
http {
定义限流区域:zone名称为vote_limit,分配10M内存(约存16万个IP状态),限制每个IP每秒1个请求
limit_req_zone $binary_remote_addr zone=vote_limit:10m rate=1r/s;
server {
listen 80;
server_name your-domain.com;
location /api/vote {
应用限流规则,允许瞬间突发5个请求进入缓冲区,超出则直接拒绝
limit_req zone=vote_limit burst=5 nodelay;
拒绝后直接返回503状态码,避免流量透传给后端应用
limit_req_status 503;
proxy_pass http://127.0.0.1:3000;
}
}
}
配置详解: $binary_remote_addr 使用二进制存储IP,节省内存;rate=1r/s 限制频率;burst=5 允许短暂的流量波动;nodelay 表示超过限制的请求立即处理,不进行延迟排队,直接拒绝,这能更有效地对抗突发刷票。
Nginx只能做简单的IP限流,针对“活动期间限制每用户只能投5票”这类业务逻辑,需要应用层介入。为了保证高并发下的原子性和性能,使用Redis配合Lua脚本是最佳实践。
安装Redis服务:
sudo apt-get install redis-server
sudo systemctl start redis-server
编写Lua脚本 check_limit.lua,该脚本利用有序集合(ZSET)实现滑动窗口算法,确保计数操作是原子的:
-- KEYS[1]: 限流Key (例如: limit:user:123)
-- ARGV[1]: 时间窗口 (毫秒)
-- ARGV[2]: 最大允许次数
-- ARGV[3]: 当前时间戳
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
-- 清除窗口之外的时间戳
redis.call('zremrangebyscore', key, 0, now - window)
-- 统计当前窗口内的请求数
local count = redis.call('zcard', key)
if count < limit then
-- 记录当前请求,value为随机字符串保证唯一,score为时间戳
redis.call('zadd', key, now, now .. math.random())
-- 设置Key过期时间,避免内存泄漏
redis.call('expire', key, window / 1000 + 1)
return 1
else
return 0
end
在Node.js后端中调用此脚本。首先安装Redis客户端:
npm install redis
后端核心代码实现:
const redis = require('redis');
const fs = require('fs');
const client = redis.createClient();
// 读取Lua脚本
const luaScript = fs.readFileSync('./check_limit.lua', 'utf8');
async function checkVoteLimit(userId, activityId) {
const key = `limit:${activityId}:user:${userId}`;
const window = 60000; // 60秒窗口
const limit = 5; // 最多5次
const now = Date.now();
try {
// eval 执行脚本,参数:脚本脚本key数量key数组参数数组
const result = await client.eval(luaScript, 1, key, window, limit, now);
return result === 1;
} catch (err) {
console.error('Redis限流异常', err);
return false; // 异常情况默认拒绝,保护系统安全
}
}
防止攻击者通过抓包获取接口URL后,直接使用脚本模拟请求。通过签名验证确保请求来源合法,并添加时间戳防止重放攻击。

前端生成签名的逻辑(假设使用HMAC-SHA256):
const crypto = require('crypto');
function generateSignature(params, secret) {
// 1. 参数按字典序排序
const sortedKeys = Object.keys(params).sort();
// 2. 拼接字符串 key=value&key=value
const str = sortedKeys.map(k => `${k}=${params[k]}`).join('&');
// 3. 计算HMAC-SHA256
return crypto.createHmac('sha256', secret).update(str).digest('hex');
}
// 前端请求示例
const secret = 'your_app_secret_key';
const payload = {
userId: 1001,
activityId: 2001,
timestamp: Date.now() // 必须携带当前时间戳
};
payload.signature = generateSignature(payload, secret);
后端验证逻辑:
function verifySignature(reqBody, secret) {
const { signature, timestamp, ...otherParams } = reqBody;
// 1. 校验时间戳,防止重放攻击(允许5分钟的时间误差)
if (Math.abs(Date.now() - timestamp) > 300000) {
return false;
}
// 2. 使用相同的参数和密钥重新计算签名
const recomputedSig = generateSignature(otherParams, secret);
// 3. 比较签名
return recomputedSig === signature;
}
攻击者可能拥有大量IP代理池,单纯依靠IP限流效果有限。通过设备指纹识别物理设备,并在关键操作前弹出滑块验证码,能有效阻断自动化脚本。
生成简易设备指纹(基于User-Agent和IP哈希):
const crypto = require('crypto');
function getDeviceFingerprint(ip, userAgent) {
const rawStr = ip + userAgent + 'salt_string';
return crypto.createHash('md5').update(rawStr).digest('hex');
}
在Redis中记录该指纹的投票行为,逻辑同上一步的滑动窗口,只是Key改为 limit:device:{fingerprint}。当检测到同一设备请求频率过高时,强制要求前端完成滑块验证码校验,校验通过后发放一个短效Token(有效期2分钟),后端只认可携带该Token的请求。
这是最后一道防线。即使前面的限流被绕过,或者出现分布式环境下的边界情况,数据库层面的乐观锁也能保证库存不会变成负数。
创建票务表:
CREATE TABLE `tickets` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`activity_id` int(11) NOT NULL,
`stock` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
);
执行扣减库存的SQL语句,必须带条件判断:
UPDATE tickets
SET stock = stock - 1
WHERE id = 1 AND stock > 0;
在代码中检查 affectedRows(影响行数)。如果返回值为1,说明扣减成功;如果为0,说明库存不足,直接返回“票已抢完”。该语句利用数据库行锁保证了原子性,绝对不会出现超卖现象。
将上述代码集成后,使用Apache Bench (ab) 进行压力测试,验证防御效果:
ab -n 1000 -c 100 http://your-domain.com/api/vote
观察Nginx日志和Redis计数。正常情况下,大部分请求应在Nginx层被拦截(返回503),漏网之鱼在Redis层被拦截,最终到达数据库的请求应严格控制在库存范围内。
上一篇: 防垃圾信息:别让网络牛皮癣糊了你的眼
易频IT社区是综合性互联网IT技术门户网站,专注分享网络技术、服务器运维、网络安全、编程开发、系统架构、云计算、大数据等行业干货,实时更新IT行业资讯、零基础教程、实战案例,为IT从业者、技术爱好者提供专业的学习交流平台。
Copyright © 2021-2026 易频IT社区. All Rights Reserved. 备案号:闽ICP备2023013482号 网站地图