ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从验证码到多层防护:垃圾注册对抗实战与风控体系设计

从验证码到多层防护:垃圾注册对抗实战与风控体系设计 做注册模块这几年最让我头疼的从来不是核心业务逻辑怎么写而是那些铺天盖地的垃圾注册。一天能收到几千条垃圾账号注册请求手机号、邮箱全给你塞满数据库里全是脏数据。一开始我用极验验证码效果好一点但接入成本和维护成本都不低而且用户流失率也上来了。后来我自己写了套防护逻辑名字就叫 FckSignups——翻译过来就是“把垃圾注册干趴下”的意思。整个思路不是靠单一验证码硬扛而是把注册防护拆成多层筛子请求层过滤、输入层检测、行为层判断、业务层兜底一层层把机器人筛掉。这篇就聊聊我这个 FckSignups 从设计到落地的完整经验特别是那些坑希望能帮你少走点弯路。适合谁看呢主要面向正在做用户注册、活动报名、表单收集功能的前后端开发者以及那些被邮箱轰炸、手机号验证码接口被人刷爆的同学。不管你是自己从零写注册模块还是想给现有系统加一层防护这套思路都能直接套用。我会把每个环节的原理、代码、参数怎么定、容易踩什么坑都讲清楚你可以当成一份可直接落到项目里的实践手册来看。1. 内容整体设计与思路拆解1.1 为什么多层过滤比单点强先说一个很反直觉的结论验证码并非注册防护的最优解甚至不是首选解。市面上很多项目一提到防垃圾注册第一反应就是上加验证码。但验证码这东西有两个绕不开的问题一是用户流失二是机器识别能力越来越强。现代打码平台接上深度学习图形验证码的识别率能做到90%以上。也就是说你费劲巴拉加了个图片验证码拦住的可能是真实用户而机器人依然畅通无阻。滑动验证稍好一点但同样存在轨迹模拟、验证码代过这些灰产服务。更别说短信验证码本身是需要花钱的——每条短信都是成本机器人把你的短信接口刷爆那就是真金白银的损失。所以我设计 FckSignups 的第一原则就是能不打扰用户就不打扰用户能不花钱就不花钱。验证码只作为最后一道兜底闸门当系统判断当前请求“存疑”时才弹出人机验证。前置的过滤尽量通过技术手段静默完成用户感知不到机器人却很难绕过。整个思路用一句话总结就是先廉价过滤再低成本判断最后重验证码兜底。成本从低到高排序依次是IP黑名单与频率限制、设备指纹与浏览器环境检测、邮箱域名黑名单与一次性邮箱识别、蜜罐字段与行为时间分析、最终的人机验证。每一层都有各自对付的机器人类型合在一起就是一个比较完整的注册防护体系。1.2 FckSignups 要解决的四个核心问题在设计这套方案时我先把垃圾注册分成了四类常见场景因为不同的垃圾注册来源特征完全不一样防护的侧重点也不同。第一类是黑产批量注册。这是最讨厌的通常发生在秒杀、优惠券、抽奖活动期间。黑产手里握着大量手机号、邮箱用脚本自动化狂刷注册接口一天能刷出上万个账号目的就是为了薅羊毛。这类请求的特征是频率极高、来源IP集中、设备指纹高度一致因为他们可能用的是同一台服务器或同一个浏览器环境。第二类是临时邮箱滥用。很多人为了绕过风控会用临时邮箱服务注册账号。临时邮箱域名有公开列表维护一个库去查是最直接的办法。第三类是短信轰炸。这类不是冲着注册来的就是单纯想搞你的短信验证码接口让你的账号被扣费扣到崩溃。特征也很明显同一个手机号或同一个IP在短时间内反复触发验证码发送。第四类是模拟器与脚本攻击。用模拟器、无头浏览器刷注册这类请求看起来像正常用户但可以通过WebDriver检测、Canvas指纹、浏览器语言时区等环境细节识别出来。搞清楚敌人长什么样防护策略才能有的放矢。FckSignups 的核心设计就是把上面四类场景对应的检测点全部做进去而且每一层都要能独立开关、可配置阈值方便按业务场景调整。1.3 总体架构一次注册请求的完整旅程为了让你更直观地理解整个流程我描述一下 FckSignups 挡在一次正常注册请求面前时后台到底发生了什么。用户在前端填完表单点提交请求先到达后端网关层。在这里走的第一个过滤器是IP 维度检查这个IP在过去5分钟内请求注册接口的次数是否超过阈值超出直接返回一个“操作频繁”的提示。通过IP检查后请求进入设备环境评分模块。后端拿到前端传来的设备指纹信息包括UA、语言、时区、Canvas指纹、WebGL信息、屏幕分辨率、是否启用Cookie、是否检测到WebDriver标记等。将这些信息打分判断“这是一个真实浏览器还是一个模拟环境”。再往下是注册字段合法性检查。邮箱格式、手机号格式、用户名黑名单都是基础操作重点是邮箱域名查询如果邮件域名在临时邮箱列表里直接拒绝。然后进入蜜罐检查。表单里隐藏了一个人类看不见的输入框正常用户不会填机器人扫描表单时会自动往所有输入框填东西一填就露馅。最后一道关口是注册频次与关联性检测同一个手机号30天内重复注册数量、同一设备指纹关联的账号数量都设了上限。通过全部检查的请求才真正走到业务层执行创建账号逻辑。整个流程对用户来说毫无感知除了极少数被判定为“存疑”的请求会弹出人机验证。这样既保证了用户体验又把垃圾注册挡在门外。2. 核心细节解析与实操要点2.1 验证码不是必需品但兜底能力必须有我见过不少团队走了另一个极端完全不用验证码结果被工会用模拟器批量注册打穿。在这里我强调一点验证码可以不是第一道防线但绝不能没有。FckSignups 里把验证码设计成一种“按需触发”的兜底策略前端配合后端返回的等级标识来决定是否展示验证码组件。具体做法是后端在完成前置静默过滤后给每个请求打一个“风险分”。风险分低于区间A的请求直接放行落在区间B的请求要求做一次基础人机验证高于区间C的请求直接拒绝。这个区间阈值可以根据业务实际情况调整比如活动期间调严一点平时调松一点。风险分的计算权重我给出一个参考配置检测项权重说明IP 黑名单命中直接拒绝不可通过加分豁免IP 短时高频请求40分5分钟内超过阈值则加分设备指纹异常25分WebDriver、Headless等标记邮箱域名信誉异常30分命中一次性邮箱名单蜜罐字段被填充直接拒绝必中机器人特征注册频率超限直接拒绝同手机号/同设备超限总风险分高于70分时弹出人机验证低于30分直接放行。这里有个细节值得说人机验证建议放在“提交后回调”的位置而不是表单初始加载时。一般用户进来先填表填完点提交才会触发风险判断需要验证再弹不需要就直接提交成功。这个体验比一进来就让你先把验证码拖一遍再填表要好得多。2.2 临时邮箱黑名单库小成本拿到大收益很多人忽略了这个点但它是我个人认为性价比最高的一道防线。临时邮箱服务的套路是用户到 temp mail 网站拿一个临时地址 → 注册你的平台 → 完成验证码接收 → 用完即弃。这类服务对用户来说确实方便但对平台来说就是大量“一次性账号”的出现严重影响用户数据质量和后续运营分析。FckSignups 维护了一个黑名单域名列表匹配注册邮箱的后缀域名。凡是命中列表的直接拦截。需要注意的是这个列表不能只靠静态维护因为临时邮箱服务商每天都在换域名。我常用的做法是定期从公开的 disposable-email-domains 仓库同步数据再结合自己的运营数据补充“被大量垃圾注册集中使用过的域名”形成一个动态黑名单。实操的时候有个坑就是域名匹配的粒度。有一类临时邮箱服务支持自定义子域名比如abctemp.example.com如果你只匹配精确域名temp.example.com那就漏了。所以匹配逻辑要同时做“精确匹配 后缀匹配”把主域名以及它的任意二级子域名都考虑进去。但如果匹配太宽也容易误伤比如某个大公司的企业邮箱服务也被列进了黑名单那真实用户就进不来了。这个问题没有完美解法只能靠“黑名单域名对应纳入隔离区”的思路命中后不直接拒绝而是要求走一遍人机验证通过了就放行。既挡住了大部分机器人又不误伤真人。2.3 蜜罐字段的隐藏艺术蜜罐honeypot是我非常喜欢的一个反机器人技巧原理极其简单效果却好得出奇。核心思路在表单中插入一个真实用户看不到的输入框用CSS把它隐藏起来比如display:none或移出可视区域同时给它一个容易让机器人“误以为”是重要字段的name属性比如website、homepage、confirm_email这种。正常用户根本看不见这个框就不会填写而自动化脚本会扫描表单里的所有输入控件以为这也是必填项顺手就填了。后端拿到发现该字段非空直接判定为机器人请求打回。这里要注意的技巧是隐藏方式不能用display:none一种有些高级爬虫会规避这种明显特征。我建议结合position:absolute; left:-9999px;或者opacity:0; height:0;多种方式混合随机选择。字段命名不能太假username_confirm、email_again这种看起来很像正常业务的字段反而更容易让机器人踩坑。千万别用honeypot、bot_trap这种连人都看得出来是蜜罐的名字。后端判断时不能只看“非空就拒绝”。有些纯浏览器的表单自动填充插件也可能误填字段。我通常会先判断填值长度和内容特征——如果里面填的是一串明显像脚本生成的乱码直接拒绝如果填的是一个合理的人类名字且其他检查项都正常再走一次人机验证兜底。蜜罐最大的优点是不打扰用户、零成本、部署简单最大的局限是只能打无脑扫描的脚本对付那些专门分析页面结构、识别蜜罐的定制化脚本就捉襟见肘了。所以它只能作为一层辅助不能用来当主力。2.4 设备指纹与行为时间分析设备指纹是个偏前端的活儿。FckSignups 的前端采集脚本会收集这些信息User-Agent、浏览器语言、插件列表、Cookie开关状态Canvas 指纹绘制特定图形后取哈希WebGL 渲染器信息屏幕分辨率与色深时区、触控支持情况是否有navigator.webdriver标记这些信息汇总后通过接口传给后端后端做一致性校验。这里有个关键点稳定性比真实性更重要。真正的用户在不同请求之间指纹是基本稳定的而脚本攻击每次请求都有可能暴露不同的指纹或者干脆缺失某些字段。检测规则可以这样定同一IP下出现超过3种不同指纹且注册频率很高就直接限流同一指纹注册账号数超过5个也触发预警。时间行为分析这一块我用得最多的是“表单停留时长”和“输入速度”。真实用户在注册页面再怎么快从加载到提交至少也要三五秒因为要填写、思考。而脚本可以在几百毫秒内完成整个表单提交。所以前端记录下页面加载时间戳在提交时带上一个form_elapsed_time字段后端如果发现这个时间低于2秒就判定为可疑。注意这个值不能只信前端传的因为脚本完全可以伪造我一般把前端传值当参考后端再结合上一次请求的间隔以及API路径访问序列做综合判断。一个更快、更隐蔽的替代方案是基于请求间隔的统计分布做阈值判断——正常用户两次提交之间的间隔方差比较大而脚本间隔非常稳定。3. 实操过程与核心环节实现3.1 后端防护层一个可插拔的注册请求过滤器接下来进入正题说说我在 FckSignups 里写的核心代码。后端我用的是 Node.js Express但思路是语言无关的你换成 Java、Python、Go 都可以照搬。核心是写一个中间件过滤器链所有注册接口请求先过这个链再进业务逻辑。下面这段代码是过滤器的入口按顺序执行各检查项// signupGuard.js const ipLimiter require(./checks/ipLimiter); const deviceCheck require(./checks/deviceCheck); const emailCheck require(./checks/emailCheck); const honeypotCheck require(./checks/honeypotCheck); const riskScorer require(./checks/riskScorer); async function signupGuard(req, res, next) { // 第一阶段硬拦截 const hardBlock await hardBlockChecks(req); if (hardBlock) { return res.status(403).json({ code: HARD_BLOCK, message: 请求已被拒绝 }); } // 第二阶段风险打分 const score await riskScorer.getScore(req); // 第三阶段按风险分策略执行 if (score 70) { return res.status(400).json({ code: CAPTCHA_REQUIRED, needCaptcha: true, score }); } if (score 30) { // 记录日志但不阻断 req.signupRisk score; } next(); } async function hardBlockChecks(req) { // IP黑名单、蜜罐、一次一密手机号直接硬拒 const checks [ ipLimiter.isBlacklisted(req.ip), honeypotCheck.detect(req.body), emailCheck.isDisposable(req.body.email) ]; const results await Promise.all(checks); return results.some((r) r true); }这里的核心设计是硬拦截和软评分分离。蜜罐命中、IP黑名单、临时邮箱这三个属于“一票否决”因为它们命中了基本可以确认是机器人或垃圾发起方。而像频率超限、指纹异常这种属于“可疑信号”需要按分值累加判断避免一刀切误伤用户。接入到路由的方法非常简洁以 Express 为例const signupGuard require(./middleware/signupGuard); app.post(/api/register, signupGuard, registerHandler);用中间件的好处是你完全可以在不修改业务代码的情况下单独开启或关闭某一层防护。比如做活动时想严格一点可以直接把阈值调低平时想放开一点阈值调高即可。3.2 前端采集与提交策略如何优雅地上报设备指纹前端部分我封装了一个collectFingerprint()方法用requestIdleCallback在浏览器空闲时采集信息避免阻塞页面加载。这个函数只是完整指纹采集的一个简化示例实际项目中还会融合更多的数据点。// fingerprint.js export function collectFingerprint() { return new Promise((resolve) { requestIdleCallback(async () { const canvas document.createElement(canvas); const ctx canvas.getContext(2d); ctx.fillText(FckSignups, 100, 100); const canvasHash canvas.toDataURL().split().reduce((sum, c) sum c.charCodeAt(0), 0); const params { ua: navigator.userAgent, lang: navigator.language, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, screen: ${screen.width}x${screen.height}x${screen.colorDepth}, canvasHash, webdriver: navigator.webdriver true ? 1 : 0, touchSupport: navigator.maxTouchPoints 0, // 可扩展更多字段 }; resolve(params); }); }); }提交时把这些指纹参数和表单数据一起 POST 到后端。需要注意的两个实操细节第一指纹采集肯定有失败场景。比如某些浏览器对 Canvas 指纹读取有隐私保护可能返回空白。所以后端对缺失字段不能直接判死刑可以按“未知”处理只要其他字段正常就放行。第二前端传上来的数据必须当参考而不是当真值。因为所有前端数据理论上都能被伪造。但后端不能因此就不收——你要做的是将指纹与后续行为做关联性校验比如下单时的指纹和注册时的指纹是否一致而不是单独信任它。前端还有一个必须做的环节给表单提交按钮加一个最短提交时间控制。用户在页面停留不足1.5秒就提交在后端逻辑里直接标记为可疑。这个“最短提交时间”要放在 sessionStorage 里由前端先写入提交时后端校验时间差但同样不能作为唯一证据只能作为风险加分的因子之一。3.3 频率限流接口防刷的最后一根稻草频率限流看起来是所有中间件里最基础的但它真的能解决大量问题尤其是短信和邮箱验证码的接口防刷。我用的是一个基于内存的双层计数结构具体设计如下// rateLimiter.js class SlidingWindowCounter { constructor(windowMs, maxAttempts) { this.windowMs windowMs; this.maxAttempts maxAttempts; this.timestamps new Map(); } hit(key) { const now Date.now(); const list this.timestamps.get(key) || []; while (list.length 0 now - list[0] this.windowMs) { list.shift(); } if (list.length this.maxAttempts) { return { allowed: false, remainingMs: this.windowMs - (now - list[0]) }; } list.push(now); this.timestamps.set(key, list); return { allowed: true, remaining: this.maxAttempts - list.length }; } } const sendSmsLimit new SlidingWindowCounter(5 * 60 * 1000, 5);关键限制参数我列成一张表供参考场景窗口长度最大次数桶粒度单个IP发送验证码5分钟5次IP单手机号发送验证码1小时5次手机号注册接口提交1分钟10次IP设备指纹同设备注册账号24小时3次设备指纹实现的时候注意几个坑单机内存限流只适用于单实例部署多实例部署就得用 Redis 做分布式限流另外限流的错误响应不能暴露过于具体的信息否则容易被人推断出你内部的规则。返回一个“操作太频繁稍后再试”就好别把剩余次数、重置时间之类的细节都告诉对方。3.4 高水位策略验证码只在最危险的时刻出现最理想的产品状态是用户完全感觉不到防护系统的存在。所以在 FckSignups 里验证码默认是关闭的只有两种情况会触发第一种是在风险评分中处于“中间地带”的请求。后台返回needCaptcha: true前端收到这个标识后在当前表单上方动态插入一个验证码组件。用户完成验证后把验证凭证ticket和之前的表单数据一起重新提交后端校验通过后放行。第二种是注册接口整体压力很大的时候。我会在系统层设置一个并发保护——当注册接口每分钟请求量超过正常上限时自动将风险阈值从70拉低到30等效于给所有请求都套上一道人机验证。等流量恢复平静再自动恢复阈值。注意验证码选型上我强烈建议用“行为式验证”而不是“字符图形验证”。字符图形验证对OCR技术的拦截率已经岌岌可危而行为式验证虽然也能被高级模拟器破解但成本高至少能劝退大部分没针对性的脚本。这个“动态降级”机制我记得在秒杀活动场景里特别有用。平时大家注册账号根本不被弹窗打扰618、双11那种流量洪峰时系统自动变成严格模式。有时候同一个活动页前端没改一行代码只是后端把阈值调了一下垃圾注册量就能从每天几万掉到几百。4. 常见问题与排查技巧实录4.1 误伤真实用户羊毛没揪到先把自己人拦了这套系统上线后遇到最大的问题不是拦不住机器人而是误伤真实用户。印象很深的一次某个公司开展地推活动线下销售用同一个WiFi网络给意向用户注册账号。同一个出口IP短时间内几十个不同手机号提交注册结果被IP频率限制给全拦了。当时后台告警一片销售那边急疯了。排查下来问题出在 IP 维度限流太粗暴。真实场景里同一个办公WiFi、同一个校园网都可能出现几十人共用出口IP的情况。后来我把 IP 限流策略改了同一IP的注册频率限制从“硬拦截”改成“加分项”。只有同时满足“IP高频”且“设备指纹一致”或“用户Agent完全相同”时才走验证码兜底。如果IP高频但指纹和UA各不相同说明可能是NAT网络下的真实用户直接放行。这个改动有效解决了误伤又不给真正的批量脚本开口子。4.2 机器人定期换IP频率限制形同虚设有段时间黑产开始用代理池打注册接口IP每天换好几轮频率限制完全失效。怎么办我后面加了一道基于设备指纹的关联绑定同一个设备指纹下注册账号数量超过3个触发风控同一张身份证绑定的手机号更换设备登录时要求额外验证。这招确实有效因为对脚本来说最贵的不是IP而是“模拟一个稳定的、真实的浏览器环境”。很多批量注册脚本为了省事会复用同一个浏览器内核和指纹参数只是换IP。指纹绑定就能把这批账号串起来识别。经验是不要只看单请求特征要多看关联关系。垃圾注册的软肋不在于单个请求有多像真人而在于他们跑批量的效率逻辑必然会在多个维度上留下相似性或同一性。你要抓的就是这个。4.3 验证码服务商被打你的注册业务也会跟着瘫有一次第三方验证码服务商机房出问题导致所有需要弹验证码的注册请求全部失败用户没法注册工单一波接一波。这次事故让我明白不能把核心链路的可用性完全交给第三方。后面我做了两级缓存降级方案验证码服务接口超时超过3秒自动降级为“只校验逻辑签名”不再等待第三方结果。第三方服务完全不可用时直接放行所有中低风险的请求只拒绝“硬拦截”的请求。核心原则是注册业务宁可放进来几个垃圾账号也不能让真实用户完全进不来。垃圾账号事后可以清理真实用户流失了就很难拉回。在系统设计里永远要给“可用性”留下最高优先级。4.4 自建临时邮箱验证域名数据集的两个坑维护一次性邮箱域名黑名单看似简单其实有两个坑特别容易踩第一个是匹配到合法服务商。比如某些海外免费邮箱服务商的部分子域名也被临时邮箱平台拿来用直接全封的话正常注册用户会受影响。我的解决办法是分层处理纯黑名单域名如mailinator.com这种完全没有正当用途的直接硬拦有争议的子域名先走验证码再说不直接拒绝。第二个是域名列表滞后。新出现的临时邮箱服务没过多久就能绕过这份名单。我每周跑一次同步脚本更新列表同时在注册日志里做“注册地址使用率”分析——如果某个域名的注册量突变增高手动排查后加入黑名单。这个黑名单光靠开源项目是不够的必须结合自己的业务数据持续喂养。4.5 登录接口才是垃圾注册的隐藏重灾区项目做到后面我发现一个问题很多垃圾账号根本不是通过注册接口创建的而是先用手机号验证码拿临时账号登录进而占用系统资源。我第一次看到的时候也纳闷后来想明白了——登录接口如果设计得不好就意味着你花了大量精力守住注册口对方从后门又溜进来了。解决方式不能靠降低登录体验而是让登录接口也走一遍轻量级风险判断邮箱格式、手机号有效性、密码是否常见弱口令、登录频次是否超限、设备指纹是否与历史记录冲突。登录接口的验证码策略是同一IP连续失败5次以上才弹验证码。因为登录不像注册那么容易受到临时邮箱攻击重点防护对象变成了撞库和暴力破解。所以我建议你做注册防护时别只看注册接口。整个账号生命周期里所有可以创建或接管账号的入口都要纳入同一个风控体系里来考虑。结尾聊几句FckSignups 这个项目从一开始的简单 IP 限流慢慢演变成一个多层次的注册防护体系整个过程踩的坑比我预想的多得多。我最大的体会是不要迷信某一个安全手段所有手段都有它的边界。验证码会误伤用户IP 限流会误伤 NAT 用户邮箱黑名单可能误伤正常邮箱服务设备指纹可以被伪造。真正的安全感来自于多个检测点的交叉验证以及你对自己业务流量模型的持续了解。如果你准备做类似的东西我的建议是先从最有效的两招开始——维护一个靠谱的临时邮箱域名黑名单再加一个滑动窗口限流。这两个实现成本最低、见效最快。然后再逐步叠加蜜罐、设备指纹、风险评分这些更精细的检测。千万不要上来就上一大堆策略那不仅维护起来痛苦误伤率也会高到你怀疑人生。最后再分享一个小技巧上线前一定要造几张“垃圾注册画像表”——比如一次性邮箱注册、同一IP高频注册、WebDriver标记注册每个场景都要有对应的测试用例。我的做法是把这些用例直接改成自动化测试脚本放在 CI 流程里。每次改动风控规则后跑一遍全套测试能挡住 80% 的线上事故。希望这篇文章能让你在对抗垃圾注册的路上少走一些弯路。有什么问题欢迎在评论里交流特别是那些你遇到过的奇怪垃圾注册案例我也想涨涨见识。
返回列表