当前位置: 首页 > news >正文

上线当天注册接口被刷爆:我用滑块验证码 + 请求指纹把羊毛党拦在了网关层

上线当天注册接口被刷爆:我用滑块验证码 + 请求指纹把羊毛党拦在了网关层

上线第三个小时,注册接口的 QPS 从平时的 120 飙到 3800。验证码服务炸了,短信账单直接刷了半个月的预算。我打开监控面板,看到一波 IP 地址每秒钟都在换,但 User-Agent 永远是同一串——典型的羊毛党脚本攻击。

这次不打算讲什么"加强安全意识"的空话。直接上方案:我们在网关层做了一套滑块验证码 + 请求指纹的联动防御,把异常注册流量压回了正常水位。整个改动没有入侵业务代码,从发现问题到上线只花了 6 个小时。

攻击长什么样

先看日志。正常的注册请求,IP 分布是分散的,每个 IP 平均 3-5 次请求。羊毛党的请求长这样:

  • 同一秒内,同一个手机号被重复提交 40 次以上
  • IP 来自全国各地,但请求间隔固定为 200ms,明显是脚本控制
  • User-Agent 伪装成 Chrome,但缺少了Sec-CH-UAAccept-Language的合法组合
  • 请求体里的手机号格式全部统一,没有人类输入常见的停顿和纠错

我拉了一条统计命令,把嫌疑请求筛出来:

# 从 Nginx access.log 提取高频注册 IP(1分钟内 > 50 次)awk' $7 ~ /\/api\/register/ { ip=$1; ts=$4" "$5; gsub(/^\[/,"",ts); gsub(/\]$/,"",ts); bucket=substr(ts,1,17); # 精确到分钟 key=bucket" "ip; cnt[key]++; if(cnt[key]==1) first[key]=$0; } END { for(k in cnt) { if(cnt[k]>50) print cnt[k], k, first[k] } } '/var/log/nginx/access.log|sort-rn|head-20

结果出来,前 10 个 IP 贡献了 73% 的注册流量。这些 IP 每隔 2 分钟就换一批,但请求指纹几乎完全一致。

方案选型:为什么不用传统验证码

第一时间团队有人提议加图片验证码。我说不行。

传统图片验证码对羊毛党基本无效。现在的 OCR 识别率早就过了 95%,打码平台 1 分钱一次,脚本调用 API 就能自动过。图片验证码唯一的作用,是把正常用户恶心走。

滑块验证码不一样。它的验证逻辑不是"你认不认识这张图",而是"你的拖拽轨迹是不是人类"。羊毛党的脚本可以模拟位置,但模拟不了加速度曲线、停顿习惯、和鼠标抖动的自然分布。

我们选的方案是网关层滑块验证码 + 请求指纹双重校验。网关层做,意味着业务服务零改动;双重校验,意味着绕过一层还有第二层。

滑块验证码的网关层集成

我们的网关基于 Spring Cloud Gateway,改起来很直接。

第一步,在注册路由上加一个前置过滤器。如果检测到请求指纹异常,先弹滑块挑战,通过之后才转发到下游的注册服务。

@ComponentpublicclassAntiSpamGatewayFilterimplementsGlobalFilter,Ordered{@AutowiredprivateRedisTemplate<String,String>redisTemplate;@AutowiredprivateCaptchaServicecaptchaService;@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){Stringpath=exchange.getRequest().getURI().getPath();if(!path.equals("/api/register")){returnchain.filter(exchange);}Stringfingerprint=buildFingerprint(exchange);StringriskKey="risk:fingerprint:"+fingerprint;StringriskScore=redisTemplate.opsForValue().get(riskKey);// 高风险指纹强制过滑块if("HIGH".equals(riskScore)){StringcaptchaToken=exchange.getRequest().getHeaders().getFirst("X-Captcha-Token");if(captchaToken==null||!captchaService.verify(captchaToken)){exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);byte[]body="{\"code\":429,\"msg\":\"请完成安全验证\"}".getBytes();returnexchange.getResponse().writeWith(Mono.just(body).map(b->exchange.getResponse().bufferFactory().wrap(b)));}}returnchain.filter(exchange);}privateStringbuildFingerprint(ServerWebExchangeexchange){HttpHeadersheaders=exchange.getRequest().getHeaders();Stringua=headers.getFirst(HttpHeaders.USER_AGENT);Stringaccept=headers.getFirst(HttpHeaders.ACCEPT);StringacceptLang=headers.getFirst(HttpHeaders.ACCEPT_LANGUAGE);Stringip=exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();// 组合核心特征,做 MurmurHashStringraw=String.join("|",ua,accept,acceptLang,ip.split("\\.")[0]);returnHashing.murmur3_128().hashString(raw,StandardCharsets.UTF_8).toString();}@OverridepublicintgetOrder(){return-100;}// 确保在认证过滤器之前执行}

这段代码的核心思路:用User-Agent + Accept + Accept-Language + IP 前三段拼一个请求指纹。指纹命中高风险的,必须带合法的X-Captcha-Token才能放行。

为什么用 IP 前三段?羊毛党用的代理池 IP 经常来自同一个 /24 网段,取前三段既能聚合同一批攻击,又不会误伤正常小区宽带用户。

滑块验证的后端实现

滑块不是前端自己玩自己的,后端必须验证轨迹。

我们的验证模型很简单,但有效。把用户的拖拽过程拆成 50ms 一个采样点,记录每个点的(x, y, timestamp)。后端校验三条规则:

  1. 总时长在 800ms 到 4000ms 之间。机器脚本通常要么太快(< 300ms),要么匀速(每 50ms 固定偏移)
  2. 加速度不是常数。真实人类的拖拽有启动、加速、减速、微调四个阶段,加速度曲线是抛物线形;脚本通常是线性或正弦模拟
  3. 终点有回退微调。人类放开水印块时,有 60% 概率会有 3-10 像素的回退调整,脚本几乎不会

验证代码的核心逻辑:

publicbooleanverifyTrajectory(List<SlidePoint>points){if(points==null||points.size()<10)returnfalse;longduration=points.get(points.size()-1).timestamp-points.get(0).timestamp;if(duration<800||duration>4000)returnfalse;// 计算加速度方差List<Double>accelerations=newArrayList<>();for(inti=2;i<points.size();i++){doublev1=(points.get(i-1).x-points.get(i-2).x)/50.0;doublev2=(points.get(i).x-points.get(i-1).x)/50.0;accelerations.add(v2-v1);}doubleavg=accelerations.stream().mapToDouble(d->d).average().orElse(0);doublevariance=accelerations.stream().mapToDouble(d->Math.pow(d-avg,2)).average().orElse(0);// 人类加速度方差通常在 15-80 之间,脚本方差要么接近 0,要么异常大if(variance<5||variance>200)returnfalse;// 终点回退检测:最后 100ms 是否有负方向移动inttailStart=Math.max(0,points.size()-3);booleanhasRetreat=false;for(inti=tailStart+1;i<points.size();i++){if(points.get(i).x<points.get(i-1).x)hasRetreat=true;}returnhasRetreat;// 人类基本都会有微调}

这套规则上线后,拦截了 94.7% 的脚本注册,误判率(正常用户被拦)低于 0.3%。

请求指纹的风控联动

滑块是最后一道门,前面还需要一个识别"谁该被拦"的机制。

我们在 Redis 里维护了一个轻量的风控评分系统:

# 指纹评分规则(Lua 脚本,原子执行)localfingerprint=KEYS[1]localip=KEYS[2]localphone=KEYS[3]localnow=tonumber(ARGV[1])-- 维度1:同一指纹1分钟内注册次数localfcount=redis.call('zcount','fp:'..fingerprint, now-60, now)iffcount>3thenredis.call('setex','risk:fingerprint:'..fingerprint,300,'HIGH')return'BLOCK'end -- 维度2:同一 IP1分钟内注册次数(IP 前三段聚合)localipcount=redis.call('zcount','ip:'..ip, now-60, now)ifipcount>10thenreturn'BLOCK'end -- 维度3:同一手机号10分钟内被请求次数(撞库/批量验证)localpcount=redis.call('get','phone:'..phone)ifpcount and tonumber(pcount)>5thenreturn'BLOCK'end -- 记录本次请求 redis.call('zadd','fp:'..fingerprint, now, now..':'..phone)redis.call('zadd','ip:'..ip, now, now..':'..phone)redis.call('incr','phone:'..phone)redis.call('expire','phone:'..phone,600)return'PASS'

三个维度同时监控:

  • 指纹维度:同一个设备指纹 1 分钟内超过 3 次注册,直接标为 HIGH,后续请求强制滑块
  • IP 维度:同一个 /24 网段 1 分钟内超过 10 次注册,直接拒绝
  • 手机号维度:同一个手机号 10 分钟内被提交超过 5 次,拒绝(防止撞库和短信轰炸)

这个 Lua 脚本放在 Redis 里用EVALSHA执行,RT 在 2ms 以内,对注册接口的延迟影响可以忽略。

效果验证

上线当天晚上,我盯着 Grafana 面板看了两个小时。

攻击前的基线:

  • 正常注册 QPS:120
  • 羊毛党注册 QPS:3680(占总流量 96.8%)
  • 短信验证码发送:3400 次/分钟

防御上线后 30 分钟:

  • 正常注册 QPS:115(轻微下降是因为多了滑块步骤,但可接受)
  • 羊毛党注册 QPS:47(被滑块拦截后剩下的漏网之鱼)
  • 短信验证码发送:128 次/分钟

拦截率 = (3680 - 47) / 3680 =98.7%

更关键的是,误判率很低。我拉了一个小时的真实用户注册漏斗,完成滑块验证的用户,最终注册成功率是 91.2%。也就是说,加了滑块之后,只有不到 9% 的正常用户因为嫌麻烦而放弃——这个代价远比被羊毛党刷爆要低。

写在最后

很多人一提到防刷,第一反应是买商业 WAF 或者接第三方风控 SDK。不是说这些不好,而是它们往往需要改业务代码、加依赖、还要担心供应商的延迟和稳定性。

我们的方案全部放在网关层,业务服务连一行代码都不用改。滑块验证码自建,成本几乎为零;请求指纹用 Redis 维护,2ms 延迟;规则用 Lua 脚本原子执行,没有竞态条件。

如果你也在被羊毛党骚扰,我建议先别急着买服务。拉一下你的 access.log,看看攻击到底长什么样。很多时候,几行 awk 加上一个轻量的网关过滤器,就能解决 95% 的问题。

剩下的 5%,再考虑上商业方案也不迟。

http://www.cnnetsun.cn/news/2004458.html

相关文章:

  • 踩坑实录:Go 语言高并发+短效代理IP,数万个“幽灵连接”是怎么榨干服务器的?
  • 如何3分钟导出原神成就:开源工具YaeAchievement的完整指南
  • 测试数据生成术:合成数据工具
  • 基于Flask和MySQL的维修管理系统是否能让3-5家连锁店共用
  • 儿童护眼灯推荐哪款品牌?深度对比书客、明基、孩视宝、柏曼等主流护眼台灯,真正护眼的到底是哪几款?一篇帮你选明白,选对少花冤枉钱!
  • 手把手教你复刻电赛项目:OpenMV板球系统PID参数整定与舵机调试全记录
  • 涨薪技术|Prometheus监控之核心组件简介
  • BEV:典型BEV算法总结
  • 互联网大厂Java求职者面试全流程解析(含技术点详解)
  • Java Loom响应式安全白皮书(2024最新版):覆盖JDK 21+22+23 LTS,含37个真实CVE复现与防御代码模板
  • 常见十大ADC滤波算法C程序实现(限幅滤波_中值滤波_加权递推滤波_滑动平均滤波_一阶滞后滤波等)
  • 【2025企业级部署红线预警】:C# 14 原生 AOT 下 Dify 插件动态加载失效的4种静默崩溃场景及热修复补丁
  • CSS如何理解align-content与align-items的区别
  • 用本体层驯服企业级 AI 智能体
  • 算法打卡第八天 88.合并两个有序数组。
  • FanControl终极修复指南:快速解决传感器计数异常问题
  • 基于.NET的Windows窗体编程之WinForms数据表格
  • 华为OD机试真题 新系统 - 准备生日礼物 (Java/Py/C/C++/Js/Go)
  • # 发散创新:基于Selenium的自动化测试框架重构与实战优化在当今快速迭代的软件开
  • **用Python + DeepLearning玩转AI艺术:从代码到生成式视觉创
  • AB测试自动化革命-动态因果分流与指标自治
  • 2025届最火的五大降AI率网站实测分析
  • aibiye爱毕业是一款免费查重平台,每日检测次数无限制,AI改写功能助力论文降重更高效
  • MongoDB中什么是Hashed Shard Key的哈希冲突_哈希函数的分布均匀性分析
  • 【紧急通告】C# 14原生AOT已成Dify企业版合同SLA新增条款!未启用AOT部署的客户将于2025 Q3起暂停远程模型热更新支持——立即获取迁移检查表与ROI测算器
  • 多维度拆透渲染引擎 第一篇【维度:定义】概念正本清源 —— 渲染引擎的本质与“引擎性“
  • 基于Vue3与TypeScrt开发的Three.js汽车展厅源码
  • MusicBee网易云歌词插件:打造专业级音乐播放体验
  • 新概念英语第二册16_A polite request
  • Bilibili视频下载神器:5分钟掌握跨平台B站视频下载技巧