☰
服务端防刷四层机制:IP限流、设备指纹、验证码与风控
2026/10/12 15:24:36 网站建设 项目流程

简介:一份基于Java Web的投票系统课程设计源码,面向初学JSP/Servlet、需要完成web设计或课程项目的学生。系统启动后显示“欢迎给老师投票”页面,表格中列出教师编号、姓名与得票数,并以红色进度条直观展示得票变化,点击投票链接即可实时增加票数,形成从前台展示到后台数据更新的完整流程。压缩包共24个文件,整体约956KB,核心包含4个Java类、3个JSP页面、2个jar依赖及多个XML配置文件,并附带class文件与Eclipse工程配置,便于直接导入运行和对照学习。源码对数据库操作和公共代码进行了封装,体现分层与复用思想;同时从防刷票角度出发,引导读者分析系统可能存在的安全隐患及改进方向,适合作为课程设计、实验报告或毕业设计的参考素材。目前已有1931人学习下载,可用于快速搭建投票类功能并理解Java Web项目结构。

1. 投票系统为什么总被刷:先搞清楚对手再写代码

一个看起来平平无奇的投票页面,攻击者只需要写一个几十行的循环脚本就能把票数刷到天文数字。很多web设计教程教出来的投票系统,防刷只做到前端按钮置灰和Cookie记录,这在真实场景里几乎等于裸奔。防刷票的核心从来不在页面长相,而在服务端能不能在每次投票请求里识别出"这是同一个人换着姿势在投"。

这套投票系统源代码是一份可运行的前后端项目,包含候选人管理、投票页、实时结果展示,真正的价值在于内置了四层服务端防刷机制:IP频率限制、设备指纹识别、验证码与Token双校验、异常行为风控黑名单。适合做课程设计、企业内部评优、社区活动投票,也适合想研究服务端请求校验写法的开发者直接改造成自己的轮子。

不要指望装完就能挡住所有刷票手段,但把资源里的策略组合起来,足以拦截绝大多数脚本批量刷和手动换IP操作。下面从数据模型开始,一步步拆这台"防刷机器"是怎么把每一条投票请求卡在业务逻辑之外的。

2. 投票系统的数据模型与会话指纹:从表结构到请求链路

2.1 核心表设计:候选人表、投票记录表、限流辅助表

先看库表。这个资源里投票相关表有三张:candidate、vote_record、vote_limit。candidate存候选人信息和展示用的票数,vote_record存每一次投票的明细,vote_limit是数据库版限流的辅助表,在没有Redis的部署环境里替代内存方案。

CREATE TABLE candidate ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, description VARCHAR(500), vote_count INT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL ); CREATE TABLE vote_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, candidate_id INT NOT NULL, fingerprint VARCHAR(128) NOT NULL, ip VARCHAR(64) NOT NULL, token VARCHAR(64) NOT NULL, create_time DATETIME NOT NULL, INDEX idx_candidate_time (candidate_id, create_time) );

逻辑说明:candidate表里的vote_count是冗余计数,列表页直接读这个字段,不需要每次实时COUNT(*)。vote_record表是票数明细,每个字段都为事后追溯服务,比如同一IP投了几次、同一指纹投了几次,都可以直接从这张表查出来。注意ip字段用VARCHAR(64)是为了兼容IPv6地址,很多老项目用VARCHAR(15)存IPv4,上线后一换IPv6就报错。

参数说明:fingerprint存的是前端生成的设备指纹哈希,比如SHA-256结果,64位十六进制字符,所以128长度足够留余量。token是一次性令牌,每次投票前由服务端下发,保证一个请求对应一个令牌。建了(candidate_id, create_time)联合索引,后面风控查"某个候选人最近5分钟票数激增"时能直接走索引,不会全表扫描。

vote_limit表结构大致是这样:

CREATE TABLE vote_limit ( id BIGINT PRIMARY KEY AUTO_INCREMENT, ip VARCHAR(64) NOT NULL, fingerprint VARCHAR(128) NOT NULL, vote_time BIGINT NOT NULL, INDEX idx_ip_time (ip, vote_time) );

逻辑说明:数据库版限流就是往这张表插一条记录,投票前先查"同一IP在最近5分钟内有几条记录",超过阈值就拒绝。vote_time存毫秒时间戳,idx_ip_time索引让查询落在分钟级窗口上,还算高效。这套方案的好处是不依赖Redis,虚拟主机上也能跑,缺点是每次投票都要写数据库,流量大了之后磁盘IO会先扛不住。

参数说明:资源里默认只在没有Redis的环境自动切换到数据库版,有Redis优先走内存限流。如果你部署在云服务器上,建议直接用Redis版,性能差距在并发过百之后非常明显。这张表如果一直增长,需要定期清理一个月前的旧数据,否则索引膨胀后查询会变慢。

为什么candidate要放一个冗余票数字段?因为投票页会被反复刷新,实时统计十万人次后,数据库的COUNT(*)会越来越慢。但冗余计数也有风险:如果vote_record插入成功而candidate更新失败,票数就对不上。资源里把两步操作放进一个数据库事务,同时成功或同时失败,这比"先查旧票数,在内存里加1,再写回"的写法安全得多。后一种写法在两个请求同时读到旧票数时,必然丢一次更新,这是并发编程里最常见的翻车点。

2.2 会话标识:Cookie、Token、设备指纹的选择与取舍

匿名投票系统最难的就是识别"同一个人"。资源里用了三层标识:Cookie维持基本会话,Token校验一次性请求,设备指纹作为长期跟踪标识。为什么不用JWT?因为匿名场景下不需要携带用户身份声明,JWT的payload除了增加带宽没有实际意义,还要额外维护密钥和过期逻辑。

import secrets def generate_token(): # 生成一次性投票Token,长度16字节 return secrets.token_urlsafe(16)

逻辑说明:secrets模块是Python 3.6起的加密安全随机数生成器,比random更适合生成防猜测的令牌。token_urlsafe(16)会产生22个URL安全字符,项目中可以放在请求参数或POST体里。Token生成后存入Redis并设置300秒过期,投票完成后立刻标记为已使用,防止同一个Token被重放。

参数说明:16字节是安全底限,如果你希望更保守,用32字节也行,但要注意Token会放进URL时增加长度。过期时间要和用户输入验证码的时间匹配,300秒对正常人完全够,太长反而会让脚本有更多重试窗口。

设备指纹是整个会话体系里最重要的部分。前端采集浏览器特性生成一个哈希,存到localStorage里,每次投票随请求带给服务端。这里贴上资源里的核心JS:

function getFingerprint() { const ua = navigator.userAgent; const screen = `${screen.width}x${screen.height}x${screen.colorDepth}`; const lang = navigator.language; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const canvas = getCanvasHash(); // 绘制固定文字后读取像素摘要 const raw = [ua, screen, lang, timezone, canvas].join('|'); return hashCode(raw); }

逻辑说明:getFingerprint把UA、屏幕分辨率、色深、语言、时区和canvas哈希拼成一个字符串再做一次哈希。UA和屏幕参数容易伪造,但canvas渲染结果和操作系统字体、显卡驱动强相关,模拟成本高。攻击者如果只是改UA,指纹会保持不变;如果彻底模拟一套新指纹,又通不过后续的IP频率限制。

参数说明:screen.colorDepth也拼进去,是因为虚拟机可以伪装分辨率,但很少伪彩色深度。getCanvasHash在资源的完整代码里是绘制一段固定文字,然后读取toDataURL的结果做哈希。这段JS在页面加载时执行一次,结果同时存localStorage和Cookie,避免只存一处被用户清掉。

服务端拿指纹怎么用?资源建了fingerprint表,存储指纹的首次出现IP、最近IP、首次时间、最近次数。这个表让服务端不完全信任前端传来的指纹,而是把它和IP组合起来当参考因子。一个指纹对应多个IP是可疑的,多个指纹对应一个IP更是典型的刷票特征。

2.3 投票请求的完整链路:从页面点击到票数落库

把整个投票流程的请求链路理清楚,你就知道每道关卡挡在哪。打开投票页时,前端先请求/api/get_token,服务端下发一个一次性Token,同时生成一个四位数验证码图片,把答案存Redis。用户看到验证码后输入,点击投票按钮,前端把候选人ID、Token、验证码、设备指纹一起POST到/vote接口。服务端执行校验的顺序是固定的:

@app.post('/vote') def vote(): candidate_id = request.form.get('candidate_id') token = request.form.get('token') captcha = request.form.get('captcha') captcha_id = request.form.get('captcha_id') fingerprint = request.form.get('fingerprint') ip = request.remote_addr if not verify_token(token): return error(4001, 'Token无效,请刷新页面') if not verify_captcha(captcha_id, captcha): return error(4002, '验证码错误') if not check_vote_limit(ip, fingerprint): return error(4003, '投票太频繁') if check_risk(ip, fingerprint, candidate_id): return error(4004, '触发异常保护') do_vote(candidate_id, fingerprint, ip, token) return success({'msg': '投票成功'})

逻辑说明:Token和验证码校验开销最小,放在最前面挡掉无意义请求。频率限制和风控检查在业务写入前执行,任何一步失败都不会碰数据库。这里的顺序是刻意设计的,如果先写库再校验,被拒的刷票请求也会留下记录,后续统计和清理都是麻烦事。

参数说明:4001到4004是资源自定义的业务错误码,不是HTTP状态码。HTTP状态码统一返回200,因为服务端确实收到了请求,只是业务层面拒绝。前端根据错误码区分提示文案。有些教程会在限流时返回429,但那样前端要写额外逻辑判断,这里统一200+业务码是为了前端好处理。

do_vote函数内部再开一个事务,先插入vote_record,再更新candidate.vote_count。这个动作放在所有校验之后,保证进入事务的都是已经被判定为"正常"的请求。即便如此,事务里的两条SQL仍然要保证原子性,防止并发写库时票数丢失。

2.4 为什么不用纯前端防刷:web设计里最常见的误区

很多投票系统demo把防刷做在按钮上:点击后禁用按钮、用Cookie记录已投票、本地存储投票时间戳。这套东西在真实攻防面前一秒钟就穿了。攻击者根本不需要点击页面按钮,直接打开浏览器控制台,用fetch循环调用后端的投票接口,前端那点限制只是让普通人多按一次。

这个误区来源于把"用户体验"和"安全校验"混在一起。按钮禁用、投票成功弹窗是体验设计,安全校验必须放在控制权在自己手里的服务端。前端传上来的所有字段,包括指纹、时间戳,都不可能被完全信任,服务端只能把它们当线索,结合历史记录做判断。

资源里所有防刷门槛都在服务端,前端只负责采集和展示。这个设计原则同样适用于注册防恶意、登录防爆破、短信防轰炸,本质都是"不要信任客户端输入"。投票系统因为数据简单、直接看得见票数变化,把这个原则暴露得格外明显。如果你之前写过纯前端防刷的投票功能,现在回头想,你其实是在跟攻击者商量"请自觉不要刷票"。

3. 服务端防刷策略落地:限流、校验码与黑名单

3.1 基于IP+指纹的滑动窗口限流

防刷第一道门是限流,限流有三个维度:IP、设备指纹、全局总量。IP维度限制"同一个IP在5分钟内最多投5次",指纹维度限制"同一个指纹在24小时内最多投3次",全局维度限制"整个活动每小时总票数不超过预设值"。三个维度都触发时,以最严格的那个为准。

为什么要用滑动窗口而不是固定窗口?固定窗口把时间切成整段,假设窗口是1分钟,攻击者可以在一分钟的最后1秒投一次,下一分钟的第1秒再投一次,两次请求间隔0.2秒就穿过了限制。滑动窗口用时间戳集合来维护请求发生的时间点,每次投票只保留窗口内的记录,这样就没有边界空隙。资源里用Redis的ZSet实现:

import time import redis r = redis.Redis(host='localhost', port=6379, db=0) def check_vote_limit(ip, fingerprint, limit=5, window_seconds=300): key = f"vote:ip:{ip}" now = time.time() r.zremrangebyscore(key, 0, now - window_seconds) # 丢弃窗口外的旧记录 count = r.zcard(key) # 统计窗口内剩余投票次数 if count >= limit: return False, "投票太频繁,请稍后再试" r.zadd(key, {f"{now}:{fingerprint}": now}) # 写入本次投票 r.expire(key, window_seconds) # 防止key永久占内存 return True, None

逻辑说明:zremrangebyscore会先移除timestamp小于now - window_seconds的记录,然后zcard统计集合里剩余的元素数量。zadd的member里把指纹拼接进去,是为了在同一个IP下也能区分设备;如果不拼,两个不同设备在同一IP下投票,第二次会覆盖第一次的member,导致计数失真。expire设置窗口长度倍的过期时间,让Redis里的key在窗口过后自动消失。

参数说明:limit和window_seconds是核心调参项。默认IP维度5分钟5次,指纹维度24小时3次。如果你的活动天然需要高频投票,可以把IP窗口放大到10分钟,但绝对不要取消IP限制,否则同一局域网的人一起投,IP维度形同虚设。实际部署里这个函数会被同时调用两次,一次传IP参数,一次传指纹参数,两次都通过才算数。

资源里还带了一个数据库版限流,逻辑完全一样,只是把Redis操作换成SQL查询,适合没有Redis的环境。但数据库版在高并发下有竞态问题,两个请求同时查vote_limit表发现都少于5条,然后同时插入,就穿过了限制。所以数据库版必须配合事务或唯一约束,这一点在资源注释里写得很清楚。

3.2 验证码与Token双校验:挡住脚本自动化

限流能挡住无脑循环,但挡不住轮换代理池的脚本。资源里的第二道防线是图形验证码加Token双校验。流程是:前端请求Token时,服务端生成验证码图片下发,答案存Redis,Token和验证码答案绑定。投票时用户提交Token、验证码答案、候选人ID和指纹,服务端先校验Token是否未使用,再校验验证码答案是否正确,最后把Token标记为已使用。

def verify_captcha_and_token(token, user_answer, captcha_id): # 取出Redis里存的验证码答案 stored = r.get(f"captcha:{captcha_id}") if stored is None: return False, "验证码已过期" if stored.decode() != user_answer: return False, "验证码错误" # setnx是原子操作,只有第一次调用能成功 if not r.setnx(f"token:{token}", "used", ex=300): return False, "Token已失效,请刷新页面" return True, None

逻辑说明:verify_captcha_and_token把验证码校验和Token占用放在同一个函数里。注意顺序:先验证验证码,再用setnx占用Token。如果反过来,攻击者可以先拿到一个有效Token,在验证码校验失败的情况下不断提交,直到某次验证码猜对,但Token可能已经被之前失败的请求占用,反而把自己卡住。setnx是Redis的原子命令,同一时刻多个并发请求只有一个能设置成功,所以它天然防重放。

参数说明:captcha_id是验证码图片的随机标识,不用Token本身作为存储键,是为了让验证码和Token各自独立过期。验证码答案在Redis里存5分钟,过期后用户重新获取。token:{token}的过期时间也设300秒,但一旦被setnx占位,后续请求再拿同一个Token来就会直接被拒绝。

验证码本身不是银弹,它是用来提高脚本成本的。资源做了一个很人性化的策略:正常用户第一次投票不需要填验证码,只有当IP维度触发"5分钟内超过2次"或指纹维度触发"24小时内超过1次"时,服务端才在投票响应里带上"需要验证码"的标记,前端收到后弹出验证码层。这样普通用户基本无感,又能在脚本刚开始试探时立刻加一道门槛。

3.3 黑名单与风控规则:识别异常投票行为

限流和验证码能拦住批量脚本,但拦不住"人工投票群"或者低频率慢刷。资源里加了一个风控模块,规则写在配置里,默认有三条:

规则编号判定条件动作
R1同一IP在10分钟内投票超过20次封禁IP 24小时
R2同一指纹在1小时内投票超过10次封禁指纹 24小时
R3同一候选人在5分钟内票数异常激增(超过历史均值5倍)冻结该候选人投票入口 30分钟
def check_risk(ip, fingerprint, candidate_id): ip_count = r.zcard(f"vote:ip:{ip}") if ip_count > 20: r.set(f"ban:ip:{ip}", "1", ex=86400) return True, "IP已被临时封禁" recent = r.zcard(f"vote:candidate:{candidate_id}") if recent > 5 * get_average_speed(candidate_id): r.set(f"freeze:candidate:{candidate_id}", "1", ex=1800) return True, "该候选人投票人数激增,已触发保护" return False, None

逻辑说明:check_risk放在限流之后、落库之前。它的统计源和限流一样是Redis的ZSet,所以不增加数据库压力。封禁和冻结都用带过期时间的key实现,不需要额外定时任务清理。R3的计算方式是先看候选人过去一小时的平均投票速度,再乘5作为阈值,如果最近5分钟的票数高于这个值,说明有异常拉票或刷票。

参数说明:get_average_speed在资源里返回的是一个粗略平均值,从vote_record表按候选人分组统计一小时内的票数除以3600秒。如果表里没有历史数据,就用全局均值代替。R3的误伤率其实存在,比如某些候选人突然在朋友圈转发,票数在几分钟内翻倍,会触发冻结。资源推荐把这个窗口从5分钟拉大到30分钟,阈值从5倍提高到8倍,能在"防刷"和"防误伤"之间取平衡。

这套风控的原则是"宁可错杀,不可放过"。被封禁的IP返回一个友好的提示页,说明"当前网络投票过于频繁,已被临时限制",而不是直接拒绝连接,避免正常用户误以为网站故障。同时,封禁只存在Redis和内存里,重启后自动失效,所以不用怕误封导致永久无法投票。重要活动如果担心严重刷票,可以把黑名单同步写入数据库,管理员后台手动解封。

3.4 配置项与参数调优:不同活动规模的推荐值

防刷参数调起来有点像玄学,但有几个基准线可参考。资源里把参数全部放在config.py,方便根据不同活动规模覆盖:

活动规模建议IP限制建议指纹限制全局总量限制
班级投票5分钟3次24小时1次每小时500
社区百人活动5分钟5次24小时3次每小时2000
全网公开活动10分钟3次24小时2次每小时10000

为什么全网活动反而把IP次数调低?因为全网活动面对的攻击流量更大,代理池更充足,每个IP分到的配额必须更少才能控制总量。班级投票场景里同学都是真人,把IP限制放开到5分钟3次,是允许误操作后的重投。

全局总量限制是最后一道闸门。它的作用是:即便攻击者用了海量代理池,每一个IP只投一票绕过了所有单点限制,只要总票数在短时间内异常飙升,系统就会整体暂停投票,等管理员人工确认。没有这道闸门,前面所有策略都可能在足够大的代理池面前失效。

4. 投票系统常见刷票绕过方式与排查:现象、原因、对策

4.1 绕过现象一:同一IP换User-Agent继续刷

现象:后台日志显示某个候选人的票数持续上涨,来源IP长期是同一个,但每次请求的User-Agent都不一样,有时是Chrome、有时是Safari、有时是Python requests库。

原因:攻击者写脚本循环投票,每次随机修改User-Agent来绕过基于UA的简单识别,但代理IP池没买或者没配,导致所有请求都走同一个出口IP。

解决:把设备指纹纳入限流维度。资源里check_vote_limit同时接收IP和指纹,UA变了但canvas指纹、屏幕分辨率这些特征变不了,指纹维度会在24小时3次的限制下拦住。排查步骤是先从vote_record表里按指纹分组统计,看同一个指纹是否对应了多个UA;如果对应,基本可以确定是同一设备在刷。配合R1规则,同一IP10分钟超过20次自动封IP,这条路径就被堵死。

4.2 绕过现象二:清理Cookie和localStorage绕过

现象:用户或攻击者在浏览器里清掉Cookie后,系统提示"今日投票次数已用完"的逻辑不生效,又投了一票。技术排查看,每次请求的指纹确实不一样。

原因:资源前端把指纹同时存Cookie和localStorage,但用户清理缓存时通常两者一起被清掉,重新生成指纹后,服务端会当作新设备放行。纯粹删Cookie不删localStorage的情况也存在,但如果攻击者完整清缓存,两份都没了。

解决:指纹写入两份存储,服务端优先取localStorage里的指纹,取不到再退回IP维度。IP维度是兜底,即使指纹全清,同一个IP还是会被限流。更进一步,可以在localStorage里存一个创建时间,如果新生成的指纹与旧指纹的创建时间间隔极短(几秒内),服务端怀疑是清理重建,临时要求输入验证码。资源里实现了第一种方案,第二种写在注释里作为扩展点。

4.3 绕过现象三:并发请求穿透限流

现象:攻击者用协程或者Promise.all并发发送几十个投票请求,结果成功了一大半,频率限制没有真正拦住。

原因:经典的竞态条件。旧版限流代码"先查Redis再写入"分成两步,两个并发请求同时查询时都发现count未超限,于是都通过校验并写入,等于把限制放行了双倍流量。

解决:把"检查+占用"合并成原子操作。资源里的Redis版最终用Lua脚本解决:

local key = KEYS[1] local now = ARGV[1] local window = ARGV[2] local limit = ARGV[3] redis.call('zremrangebyscore', key, 0, now - window) local count = redis.call('zcard', key) if count >= tonumber(limit) then return 0 end redis.call('zadd', key, now, now..':'..ARGV[4]) redis.call('expire', key, window) return 1

逻辑说明:Lua脚本在Redis服务端执行,整个流程不会被其他命令打断,天然原子。zremrangebyscore、zcard、zadd、expire串行完成,并发请求到达时Redis逐个执行脚本,只有第一个请求能返回1,其余都返回0。这种做法不仅解决竞态,还减少两次网络往返,单次限流检查从三次RTT降为一次。Python侧通过r.eval传入脚本和参数即可。

4.4 绕过现象四:代理池轮换IP绕过

现象:攻击者使用代理池,每次请求换一个IP,IP维度的限流失去作用。日志里所有IP都只出现一次,但票数还是快速增长。

原因:IP限流只能限制单一IP的请求频率,面对大量随机IP时无能为力。设备指纹在此刻成为关键,攻击者如果只换IP不换指纹,指纹维度会抓住它;如果连指纹一起换,就需要验证码来增加成本。

解决:资源里的策略是"指纹维度从严、IP维度从宽"并同时开启验证码。当IP维度检测到同一指纹在多个IP间跳转时,R2规则会触发封禁指纹。更严格的做法是统计"指纹与IP的对应关系",正常场景一个指纹对应一两个IP是正常的,对应几十个IP,直接判定为代理池特征。资源里风控模块的check_risk里加了一个计数逻辑,超过阈值就封禁。代理池的维护成本并不低,这一层策略通常足够劝退大多数刷票者。

4.5 绕过现象五:直接重放Token

现象:攻击者抓包拿到一次投票请求的Token和验证码,然后手动重放这组数据,连续提交多次,居然每次都提示投票成功。

原因:Token校验逻辑只检查了Token是否存在、是否过期,没有检查Token是否已经被使用过。后端的验证码校验也只在Redis里比对答案,没有在比对通过后把验证码标记为失效。

解决:把"验证码比对通过"和"Token占用"合并成一步原子操作。资源里的verify_captcha_and_token函数里已经用setnx占坑,同一个Token第二次来就发现已经used,直接拒绝。验证码也一样,比对通过后立刻delete掉Redis里的键,这样同一个验证码答案不能再用第二次。排查时看日志里是否有相同Token连续出现多次,有则说明这个漏洞被人利用了。

4.6 排查工具:日志分析与Redis监控

刷票绕过之后,最怕日志里看不出线索。资源在投票关键路径上打印了JSON日志,字段包括IP、指纹、UA、Token、候选人ID、校验结果。排查时先看失败日志,再看成功日志里的异常模式。推荐用这个命令统计IP分布:

cat vote_*.log | jq -r '.ip' | sort | uniq -c | sort -rn | head -20

逻辑说明:jq从每行JSON日志里提取ip字段,sort排序后uniq -c统计每个IP的出现次数,sort -rn按数字降序。输出结果里如果前几个IP的计数远超其他IP,基本可以锁定刷票源。再按指纹字段统计一次,能定位到具体是哪个设备。这条命令不需要写Python脚本,纯命令行就能完成初筛。

Redis侧的监控同样重要。限流key的剩余数量和过期时间能反映规则是否正常工作:

redis-cli keys "vote:*" | head redis-cli zcard vote:ip:127.0.0.1 redis-cli ttl vote:ip:127.0.0.1

如果zcard返回的数量远大于配置的limit,说明限流逻辑有问题或者key前缀写错。TTL都快到期时,key会自动消失,这是正常现象。血泪经验:日志里一定要记指纹和IP两部分,只记IP不记指纹,代理池绕过时你根本分不清哪些流量是同一个攻击主体。

5. 上线前的压测与验证:用脚本自己先刷一遍

5.1 用Python脚本模拟刷票攻击

环境部署完成后,先别急着推广,用模拟攻击脚本自己打一遍。资源里附带了一个轻量压测脚本,核心是循环请求投票接口,观察不同阶段的返回码:

import requests import time url = "http://localhost:8000/vote" candidate_id = 1 headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"} for i in range(50): resp = requests.post(url, data={"candidate_id": candidate_id}, headers=headers) print(f"第{i+1}次请求: {resp.status_code} - {resp.text[:50]}") time.sleep(0.1)

逻辑说明:正常配置下,前5次请求会返回成功,第6次开始被限流拦截,响应体里出现"投票太频繁"。如果你跑完50次全部返回成功,说明限流逻辑没生效,或者限流器被放在了投票成功流程的后面。这个脚本没处理验证码,所以只能测IP和指纹维度;想测验证码,需要额外写一个先获取图片再识别答案的脚本,但多数时候看到响应里出现"请输入验证码"就已经证明验证码逻辑被触发了。

压测后别忘了清掉测试数据,否则Redis里的key会把你的真实IP继续限流几分钟。

5.2 检查Redis中的key状态

压测结束后,进Redis确认滑动窗口数据正确:

redis-cli keys "vote:*" | head redis-cli zcard vote:ip:127.0.0.1 redis-cli ttl vote:ip:127.0.0.1

逻辑说明:先确认vote:开头的key存在,再看对应IP窗口内的计数是否等于5,以及TTL是否接近300秒。如果计数是5说明前5次通过、后续被拒,符合预期。如果计数是50,说明每次请求都写入了ZSet,限流压根没拦,回去检查key拼接和调用顺序。

然后再做一个极端测试:用不同User-Agent和Cookie反复请求,确认指纹维度仍然生效;用多线程并发请求,确认Lua脚本原子限流能扛住;最后把日志导出来跑一遍jq统计,确认没有任何明显绕过痕迹。这一套流程走完,基本可以放心上线。

从那以后我每次部署投票系统,都会强制走一遍这个流程:先跑50次循环请求看限流,再造几个空指纹看是否穿透,最后清一次Redis再压一轮。这些步骤看着笨,但每次都让我在正式开场前发现至少一两个配置遗漏。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询