当前位置:网站首页 >  攻略

十分钟实战:高并发场景下防刷票攻击全攻略

时间:2026年06月03日 14:11:48 来源:易频IT社区

架构设计思路

防刷票的核心在于“层层设防”,单一的手段很容易被绕过。一个成熟的防御体系必须包含网关流量清洗、应用层精准限流、请求合法性校验以及数据库层面的最终兜底。本文将基于Linux环境,使用Nginx、Redis、Node.js和MySQL构建一套完整的防御方案,确保每一步都能直接落地。

一、第一道防线:Nginx网关层限流

在请求到达业务代码之前,利用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 表示超过限制的请求立即处理,不进行延迟排队,直接拒绝,这能更有效地对抗突发刷票。

二、第二道防线:Redis+Lua实现精准滑动窗口限流

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号 网站地图