3.4 防刷:验证码与风控初探
场景:十个账号抢走一半库存
第二场秒杀复盘时我们拉了下单账号列表,前十个账号各抢了 5 件以上(限购规则被绕过是因为账号池换了设备标识),IP 遍布各地但提交时间间隔精确到毫秒级——明显的脚本团伙。普通用户根本点不过它们,活动公平性荡然无存。更可气的是这些账号随后在二手平台加价转卖,品牌口碑受损。防刷从此与防超卖并列为库存安全的两条红线。
方案:四层防刷体系
防刷是攻防博弈,单点防御必破。我们的做法是把难度分层,每层增加攻击成本:
| 层级 | 手段 | 拦截目标 | 成本 |
|---|---|---|---|
| 接入层 | IP 限流与 UA 过滤 | 低级脚本 | 极低 |
| 行为层 | 验证码与人机校验 | 自动化点击 | 低 |
| 账号层 | 设备指纹与注册时长 | 账号池 | 中 |
| 业务层 | 下单频次与收货地址聚类 | 团伙 | 中 |
我们的实战配置组合是 Nginx 限流、Token 验证码、设备指纹三管齐下。验证码只在秒杀提交前置,其余场景不弹,平衡体验与安全。验证码服务商自带人机识别能力,接入成本比想象中低。我们在选型时对比过三家,最后选了文档最完善、遇到问题能找到人的那家,而不是价格最低的那家,事后的几次工单证明了选择是对的。
分层防刷的核心思想是把攻击者的成本抬高到无利可图。一个脚本团伙想绕过我们的四层防御,需要养号七天、准备独占设备、通过人机验证、还要服从频次限制,单台设备一晚最多抢几件,比起直接雇人排队没有明显优势。防刷做到这个程度,攻击者会主动找更软的柿子捏,这就是最好的结果。同样地,我们自己复盘时也会扮演攻击者角色,定期用脚本演练攻击自己的秒杀接口,验证各层防御是否真的生效,这种红蓝演练的成本很低但收获不小。演练还发现过一个啼笑皆非的问题:我们自己写的测试脚本因为写得过于高效,反被验证码规则挡住了,说明防御确实在起作用,团队当天心情不错。
配置:秒杀前置校验链
下单前的校验设计成中间件链,任何一层失败都立即返回,验证码校验在最前面。中间件链的顺序有讲究:越便宜的校验越靠前,越昂贵的越靠后。比如验证码校验涉及外部服务调用,虽然是异步令牌验证,耗时仍在十毫秒量级;而账号注册时长判断是纯内存查询,几乎零成本。我们把所有便宜的本地校验全部放到验证码之前,能先挡掉的请求就不要花验证码的钱。这种精打细算在洪峰期会放大成显著的成本差异。下面是校验链完整顺序的示意:
| 顺序 | 校验项 | 成本 | 命中率 |
|---|---|---|---|
| 一 | 接口限流 | 极低 | 洪峰期最高 |
| 二 | 注册时长 | 极低 | 低 |
| 三 | 频次熔断 | 低 | 中 |
| 四 | 设备指纹 | 中 | 中 |
| 五 | 验证码 | 中 | 仅可疑流量 |
class SeckillGuardChain
{
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
if ($this->captcha->verify($request->captcha_token) === false) {
return $this->fail(4001, '请完成验证');
}
if ($user->created_at > now()->subDays(7)) {
return $this->fail(4002, '新账号暂不参与秒杀');
}
if (! $this->deviceRisk->check($user->id, $request->fingerprint())) {
return $this->fail(4003, '环境异常,请联系客服');
}
return $next($request);
}
}
风险名单的维护是后台简单规则:同一设备指纹下超过 3 个账号、同收货地址近 7 天多单、下单频次异常的账号,自动加入观察名单,需要人工复核才放行。规则不复杂,但挡住了九成以上的常见团伙。名单系统刚上线时数据比较少,规则的威力显现不出来,跑了一个月积累了足够的历史数据后,整个体系才开始发挥价值。这也提醒我们:风控系统是养出来的不是建出来的,越早积累数据越早受益。
| 规则 | 判断逻辑 | 命中处置 |
|---|---|---|
| 新号门槛 | 注册不足 7 天 | 拒绝 |
| 设备聚集 | 一设备多账号 | 观察 |
| 地址聚集 | 同地址高频下单 | 观察 |
| 频次异常 | 单账号分钟级高频 | 拒绝并冷却 |
观察名单的样子很精妙:不是直接拒绝,因为误伤真实用户的代价更高,而是让订单转入人工复核,复核通过照常发货。审核小队由两名同学轮值,高峰期一天也就处理几十单,人力成本完全可控。审核结果是规则迭代的依据:误伤多的规则调宽,命中准的规则调严,形成正向循环。审核动作本身也要可追溯,每一条审核结论都写进数据库,谁审的、依据什么、结论是什么,全部留痕,既是团队内部的经验沉淀,也是处理后续争议时的证据。
聊一个真实的案例。有次秒杀结束后我们分析了前 20 名下单记录,发现其中 14 名命中了观察名单,这些账号注册时间集中在活动前三天、设备指纹高度相似、收货地址都在同一个小区,明显的团伙行为。人工复核后这 14 单全部取消,库存回补后做了第二场返场活动,真实用户口碑反而更好了。如果没有这套名单体系,这批库存就被黄牛吃掉变成了二手平台的加价商品,品牌形象受损。这件事之后团队对风控的态度从被动变为主动,每周固定花一小时复盘命中数据,规则迭代节奏稳定下来。主动复盘比被动救火的价值大得多,这个认知同样适用于库存与监控体系的建设。
踩坑:防刷实践的三个坑
坑一:验证码挡住了真实用户。 一开始所有下单请求都强制滑块验证,移动网络下发成功率只有六成,大量真实用户卡在验证环节。优化方向是动态门槛:领取过优惠券的老用户免验证,新设备或高频账号才弹验证码,转化率回升。动态门槛的规则也不要太绕,我们的经验是规则越少越好维护,越简单越好解释,运营同学能一句话说清的规则才是好规则。
坑二:风控规则误伤批量退货地址。 一开始有几个企业采购客户被规则误伤,我们给企业认证用户开了白名单。顺带说个数据上的经验:风控规则上线评估误伤率时,一定要同时看绝对数量而不只是百分比。百分之零点一的误伤率听着很小,但放在十万人参与的活动里就是一百个真实用户,每一个都可能写一篇长长的客诉邮件。所以我们早期把所有新规则都先放在观察模式跑两周,只记录命中不实际拦截,评估清楚再开启拦截模式。
坑三:与黄牛的军备竞赛耗尽了人力。 有两个月我们与脚本团伙反复对抗,规则越加越复杂,维护成本直线上升。后来想通了:秒杀的本质是营销活动,成本预算有限的小团队不值得做重风控。保住限购规则与公平性的底线,其余通过限流抬高攻击成本即可。把主要精力放回系统稳定上,是更正确的取舍。
坑四:风控规则上线匆忙,直接误伤整批老用户。 有次为了应对一个新出现的脚本团伙,一个新规则当天上线,结果命中了一大批多年老客户,其中还有几个超级会员,客诉邮件那天下午就到了老板邮箱。从此团队立下铁律:新规则一律先在观察模式跑够三天,看清楚命中画像再决定是否开启拦截。风控的试错必须在安全的环境里完成,线上用户的信任买不回来。这件事还有一个后续:我们给所有拦截类规则加了熔断开关,命中量在十分钟内异常飙升时自动回退到观察模式,相当于给规则本身也装上了保险丝。
小结
防刷是成本与收益的平衡:分层抬高攻击成本,保住限购与公平底线,误伤真实用户比放过黄牛更糟。验证码按需触发,名单管理加人工复核,规则上线前先评估误伤率。小团队不必追求企业级风控,守住底线就够。最后分享一个心态上的转变:我们曾经把黄牛当成必须消灭的敌人,投入大量精力对抗;后来把目标调整为抬高攻击成本、让攻击变得无利可图,整个人都轻松了。秒杀活动的本质是花预算买流量与口碑,防刷只是保障手段,一定要正确分配预算与人力。系统稳定永远排在反黄牛之前,这个优先级一旦颠倒,就会在下次大促翻车。防刷规则上线后还要持续看数据,每周复盘一次误伤率与命中率,让规则跟着业务一起迭代。还有一个配置层面的建议:所有风控规则都要带一个开关和一个生效时间,大促当晚可以一键降级或关闭。我们遇到过一次规则配置错误导致整批真实用户被拦,当时规则写死在代码里,回滚只能靠发版,十几分钟内客服电话就打爆了。从那以后规则全部走配置中心,带灰度比例和自动熔断开关,误伤率超过阈值时自动停掉半数规则。防刷系统的可控性与它的检测能力同样重要,一个关不掉的风控系统比没有风控更危险,这一点怎么强调都不过分。最后一点经验:风控规则的收益要用数据说话,每月输出一份拦截报告,命中多少、误伤多少、挽回多少损失,有了这份报告,防刷工作在团队里才有持续投入的说服力,否则很容易在一次误伤事件后被全盘否定,这是我们亲历过的教训。
