服务端防刷四层机制:IP限流、设备指纹、验证码与风控
发布时间:2026/10/12 15:24:54 锦皓数字建站

简介一份基于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里每次投票随请求带给服务端。这里贴上资源里的核心JSfunction 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(hostlocalhost, port6379, db0) def check_vote_limit(ip, fingerprint, limit5, window_seconds300): key fvote: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时服务端生成验证码图片下发答案存RedisToken和验证码答案绑定。投票时用户提交Token、验证码答案、候选人ID和指纹服务端先校验Token是否未使用再校验验证码答案是否正确最后把Token标记为已使用。def verify_captcha_and_token(token, user_answer, captcha_id): # 取出Redis里存的验证码答案 stored r.get(fcaptcha:{captcha_id}) if stored is None: return False, 验证码已过期 if stored.decode() ! user_answer: return False, 验证码错误 # setnx是原子操作只有第一次调用能成功 if not r.setnx(ftoken:{token}, used, ex300): 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(fvote:ip:{ip}) if ip_count 20: r.set(fban:ip:{ip}, 1, ex86400) return True, IP已被临时封禁 recent r.zcard(fvote:candidate:{candidate_id}) if recent 5 * get_average_speed(candidate_id): r.set(ffreeze:candidate:{candidate_id}, 1, ex1800) 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绕过现象攻击者使用代理池每次请求换一个IPIP维度的限流失去作用。日志里所有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}, headersheaders) print(f第{i1}次请求: {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再压一轮。这些步骤看着笨但每次都让我在正式开场前发现至少一两个配置遗漏。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。