ARTICLE DETAIL

资讯详情

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

开源反垃圾注册网关FckSignups实战:四层规则拦截与调优

开源反垃圾注册网关FckSignups实战:四层规则拦截与调优 我见过太多人把垃圾注册当成“售后问题”处理等账号堆起来了再删封一个算一个结果永远删不完。真正该做的是把防线挪到注册入口本身在账号真正落地之前就把自动化流量拦住。这篇文章要聊的 FckSignups就是一套把这条防线落到实处的开源反垃圾注册网关方案。它适合自己运营网站、App、小程序并且开始被批量注册骚扰的团队或个人开发者。先说清楚FckSignups 这个名字确实很“上头”是那种被垃圾注册气到摔键盘之后起的项目名。英文圈里不少开源仓库都有这种泄愤式命名没必要对字面意思做太多解读重点在于它做的事不靠验证码、不靠人工审核而是把注册请求放进一条多层检查链路里用规则和特征自动打分把低质量的批量注册挡在门外。这篇文章不是项目的官方文档而是我把它接入生产环境之后从选型到调优整个过程的复盘所有方案细节都是我在实际部署中验证过的。1. 垃圾注册为什么必须在前端入口解决账号库一旦被污染后面全是成本1.1 垃圾账号带来的不只是“难看”而是连锁成本很多人对垃圾注册的认知停留在“后台多了一些僵尸账号”但实际的影响远不止如此。我自己的站点是社区型产品注册后用户可以发帖、私信、点赞。第一波垃圾注册爆发时我凌晨爬起来看了一眼数据库新增的账号几乎全带着营销签名头像清一色是诱导链接。这批账号如果放任不管下一步就是批量发帖、批量私信、批量违规内容刷屏严重的话服务器带宽、数据库连接、缓存吞吐都会被拖垮。更隐蔽的问题在于账号库被污染之后你做任何精细化运营都会失真。比如想给“新注册用户”推新手任务结果推送给一半都给了机器人想统计用户的留存和活跃数据里混进大量非人类行为下游分析全部失去参考意义。也就是说垃圾注册不清理成本是会滚雪球的。等到账号已经建立起来再去批量删除还要考虑关系链、发帖记录、举报记录清理操作本身还有可能误伤正常用户代价非常高。1.2 注册入口为什么是最佳拦截位置账号从提交注册到真正激活中间要经历表单提交、数据校验、数据库写入、邮件/短信发送等多个环节。大多数垃圾注册脚本只走到了“表单提交”这一层它们对注册成功率和后续养号根本不讲究只追求“能提交进去就行”。所以入口处的拦截性价比极高——在写入数据库之前挡掉后面所有的垃圾清理成本都直接省掉了。FckSignups 的核心思路就在这它不解决“账号已经产生之后怎么办”而是解决“账号怎么被拒绝在写入数据库之前”。这也决定了它的架构形式——它不是一个独立服务而是一段挂载在注册接口前面的检查中间件跟你的注册逻辑天然耦合但又能独立开关和降级。1.3 用它之前你要有心理预期有一个预期管理必须先讲清楚FckSignups 不是装上就万事大吉的工具。它能把 90% 以上的批量脚本干掉但需要你在头一到两周持续观察日志、调阈值、处理误杀。它不是验证码服务不会一上来就粗暴打断所有可疑用户更像是一个“流量过滤器”需要你根据自己业务的用户特征去设置判断逻辑。如果你是那种完全不想碰运维、不想看日志、只想加上就睡的开发者那直接上第三方验证码是更省事的路线。但如果你跟我一样不想把验证成本转嫁给真实用户也愿意花点时间调优这套思路值得好好看下去。2. 为什么放弃传统验证码验证码把成本丢给真人规则网关把成本留给机器人2.1 图形验证码的真实处境前几年大家防垃圾注册的第一反应就是上验证码图形验证码、滑块验证码、行为验证码轮番上阵。但到了今天我几乎不推荐再往自己的站点里塞图形验证码了。原因很简单识别成本已经低到机器人几乎不受影响而识别不通过时承受代价的永远是真人用户。图形验证码的识别服务早就产业化了几块钱一千次的打码接口到处都是批量注册脚本直接调用识别率比真人还稳定。滑块验证码稍微好一点但同样有成熟的模拟轨迹方案绕过。更关键的是验证码把判断压力全部转嫁给了用户网速慢时刷不出来老花眼看不清字符手机端输错几次就得重来。真实用户想注册却注册不进去每一次失败都是一次流失。2.2 规则网关与验证码的本质区别表格比一下就很清楚维度传统验证码规则网关方案对真人干扰高每次注册都增加操作步骤低正常用户几乎无感知对机器人识别率中等且持续下降高针对自动化脚本特征重点打击接入成本需要买服务/接SDK自建规则引擎可离线运行可调性基本不可调阈值、规则全部可配失败恢复用户被卡死体验断裂可通过降级策略恢复规则网关的核心思路是不去跟机器人比“它认不认得出这张图”而是去比“这个请求的行为像不像一个真实的人”。机器人注册最大的弱点是什么是它太快、太一次性、太不讲上下文。它不会像真人一样花三秒钟看一眼表单不会按照浏览器的默认逻辑去带各种请求头也不会规规矩矩地只提交一个表单字段而不触碰那些其实存在但被人为隐藏的区域。2.3 我选定规则网关时的三个判断标准当时我选技术方案只看了三条。第一不能依赖外部服务万一第三方挂掉不能正常注册第二正常用户注册的额外耗时不能超过 500 毫秒第三所有拦截行为必须可记录、可回放、可调整。传统验证码第一点就已经被淘汰了第二点也很难做到而 FckSignups 这套网关设计正好满足全部三条。这里也提醒一下不要盲目追求“零验证码”。验证码在降级场景里依然是有效工具比如某个 IP 已经触发了风险规则可以只对这次请求弹一次验证码作为最后一道兜底。好的方案不是二选一而是用规则网关做主流拦截用验证码做小众兜底。3. 核心拦截逻辑逐层拆解从请求头到行为指纹的四道关卡FckSignups 的拦截链路并不是一条规则走到底而是分了四层协议与环境特征、静态黑名单、蜜罐与行为时间、IP 信誉与频率限制。每一层只负责筛选一部分明显异常的流量全部通过之后才把请求交给真实的注册逻辑。下面是我实际部署中每一层的详细拆解。3.1 第一层协议与环境特征检查这层是成本最低、见效最快的一层。绝大多数注册脚本并没有用真实的浏览器环境去跑而是直接用 Python、curl、httpie 之类的 HTTP 客户端脚本或者用无头浏览器但未做任何指纹伪装。我在这一层主要检查三个维度User-Agent 是否属于主流浏览器内核是否包含python-requests、curl、wget、Go-http-client、Scrapy这类明显的脚本特征关键安全头是否完整比如Accept-Language、Sec-Fetch-Site、Sec-Fetch-Mode、Sec-Fetch-Dest正常通过浏览器发起的请求几乎都会带上这些字段Content-Type是否为注册表单预期的application/x-www-form-urlencoded或application/json脚本往往在这里偷懒出现怪异取值。示例代码大致长这样function checkProtocol(req) { const ua req.headers[user-agent] || ; const botMarkers [python-requests, curl, wget, Go-http-client, Scrapy, okhttp]; for (const marker of botMarkers) { if (ua.includes(marker)) return { blocked: true, reason: UA包含脚本特征: ${marker} }; } const acceptLanguage req.headers[accept-language] || ; if (!acceptLanguage) return { blocked: true, reason: 缺少Accept-Language头 }; const fetchSite req.headers[sec-fetch-site] || ; if (fetchSite fetchSite ! same-origin fetchSite ! none) { return { blocked: true, reason: 跨站提交注册表单 }; } return { blocked: false }; }这里要注意不能一看到缺某个头就立刻拒绝。有些老版本手机浏览器、微信内置浏览器、企业安全软件代理发送的请求头并不完整直接拦截会误伤。我的经验是第一层只用于“记录特征并加分”而不是立刻拒绝。真正触发拒绝阈值的是多层分值累加这一点后面会细讲。3.2 第二层静态特征与黑名单引擎第二层处理的是内容层面的数据。每个注册请求里至少有两个内容是可控的用户名和邮箱。批量注册脚本有一个通病就是不会精心构造这些字段。它们往往是找一个单词列表或者随机字符串生成器拼出一个看起来“合法”但实际毫无含义的用户名邮箱则集中在某些支持快速注册的免费域名。我在这一层维护了几组数据邮箱域名黑名单比如.ml、.tk、.ga、.cf、.gq这类免费顶级域名文本关键词黑名单比如用户名和邮箱里包含营销词、违禁词、乱码信号随机串识别比如用户名是 8 到 16 位无意义字母数字组合且没有任何可读音节。邮箱域名黑名单不用自己硬维护直接引用公共的 disposable email 域名列表就能覆盖大部分风险。关键词黑名单则需要结合自己业务场景定制比如我之前做社区很多垃圾注册都带着“代开发”“刷量”“跑分”这些词直接塞进黑名单词库即可。一个高效的做法是把第二层做成打分制而不是黑白分明function scoreStaticFields(user) { let score 0; const emailDomain user.email.split()[1] || ; if (disposableDomains.has(emailDomain)) score 60; if (/^[a-z]{2}[0-9]{1,3}.\.(ml|tk|ga)$/i.test(user.email)) score 40; const userName user.username || ; if (/[营销词1|营销词2|刷量|代刷]/.test(userName)) score 50; if (/^[a-z0-9]{10,16}$/i.test(userName) !/[aeiou]{2,}/i.test(userName)) score 30; return score; }3.3 第三层蜜罐与交互行为时间第二层识别的还是内容层面的异常第三层则直接验证“这个人到底有没有真的在看这个表单”。这是我从实战里觉得最可靠的一层因为脚本很难模拟出真人浏览表单时的交互特征。蜜罐字段是经典做法在注册表单里放一个真实的 input 字段但用 CSS 把它隐藏到屏幕外同时给它一个看起来正常的名字比如website、company_website这种。真人用户永远看不到这个字段自然不会填但无脑采集表单的自动化脚本会把页面上所有字段都抓下来然后一项项填完提交。所以一旦蜜罐字段有值基本可以直接判定为机器人。时间维度同样关键。真人从打开注册页面到完成提交至少需要 3 到 8 秒这还不包括阅读表单的时间。而批量脚本从页面加载到提交往往都在 1 秒以内。我实际观察到的垃圾注册请求大部分提交耗时小于 800 毫秒。所以我在后端记录了表单页的渲染时间和注册请求的到达时间两者相减之后作为一个重要判分维度。划重点这个“前端表单渲染时间”必须由后端生成并通过加密参数回传不能只靠前端传一个form_start_time否则脚本完全可以伪造。我用的方案是后端生成一个小时级有效的签名 token里面带上当前时间戳注册请求提交时校验 token 并计算时间差。3.4 第四层IP 信誉与频率限制最后一层是做整体压制。即使前几层都没拦住只要一个 IP 在短时间内反复提交注册一定有问题。频率限制的实现本身很简单但要注意两个细节一个是窗口粒度的设置另一个是共享出口 IP 的误杀控制。我把频率限制分成几档单 IP 每 60 秒最多 3 次注册请求单 IP 每 24 小时最多 10 次注册请求超过阈值后返回429 Too Many Requests并附带Retry-After头。IP 信誉方面我会把请求 IP 的 ASN 归属做一个缓存查询。普通的家庭宽带用户和移动网络用户ASN 通常是本地的运营商而大量注册脚本跑在云服务器、数据中心 IP 上ASN 归属是阿里云、腾讯云、AWS、DigitalOcean 这类服务商。家庭用户走云服务商 IP 的概率极低所以对数据中心 IP 段可以适当提高风险分。这里要特别提醒共享 IP 的问题公司办公网络的出口 IP 是同一个大学校园网的出口也可能集中如果单 IP 每天 10 次注册的限制太死一个办公室里有 11 个真实用户同时注册第 11 个人就会被误杀。所以频率限制的阈值不能拍脑袋要看自己业务的真实并发情况实在不确定就从宽设置宁可让少量漏网垃圾进来也不要误杀真人。四层逻辑全部走完每个请求会拿到一个总分。我的阈值设定是分数区间处置动作0 - 30放行31 - 70进入观察列表记录日志不拦截71 - 100返回风险提示要求二次校验100 以上直接拒绝直接拒绝的比例控制在整个注册流量的 10% 以内比较健康如果超过这个比例大概率是规则设置得太激进需要进入调优环节。4. 接入实战把 FckSignups 塞进现有注册链路的关键步骤讲完原理接下来是动手环节。我实际把这套网关以三种方式接入过不同系统直接在注册接口内集成、作为 Express 中间件、以及用反向代理层做无侵入接入。下面逐一展开。4.1 方式一注册接口内直接集成如果你的系统是 Node.js/Express、Spring Boot、Django 这类单体应用最简单的方式是将检查逻辑写成一个纯函数在注册接口入口处调用决定放行还是拦截。这种方式的好处是改动量小、逻辑直观、不涉及额外的网络链路但坏处是跟业务代码耦合较紧后续想升级规则库需要重新发版。我实际用的 Express 伪代码如下const signupGuard require(fck-signups); app.post(/api/register, signupGuard(), async (req, res) { const { username, email, password } req.body; // 核心注册逻辑 const result await createUser({ username, email, password }); res.json({ code: 0, data: result }); });这里的signupGuard()返回一个中间件函数内部会把请求上下文交给前面说的四层检查管道根据总分析出pass、review、blocked三种结果。review结果最常见的是在前端弹一个“请完成简单的算术验证”的小浮层二次校验通过后再放行到注册逻辑。4.2 方式二独立中间件代理如果你的系统是微服务架构或者注册逻辑是多个服务共同协作完成的不想在业务服务里夹带风控代码那就把 FckSignups 包成一个独立的服务放在注册接口前面用 Nginx 或者网关层把/api/register的请求先反向代理到它那里通过之后再由它转发到真实业务服务。这时候整个链路变成客户端 - Nginx - FckSignups 网关 - 业务注册服务客户端感知不到中间多了一层服务对业务服务来说 FckSignups 只是又加了一个上游代理所有原有的鉴权、接口签名逻辑都不受影响。这个方案适合已经有成熟网关体系的团队运维成本相对高但边界清晰风控规则可以在不影响业务代码的情况下独立升级。4.3 方式三无侵入反向代理如果连 FckSignups 独立服务都不想起直接用 Nginx 的 Lua 模块或者 OpenResty也可以在前置层做一部分频率限制和请求头检查。不过复杂度会明显高后面做蜜罐字段、内容黑名单时会很别扭因为你要在 Nginx 层解析 POST body这不仅是能力问题还会引入新的性能开销。我个人的建议是单体应用用方式一微服务用方式二尽量不要把复杂风控硬塞进 Nginx 层。Nginx 层适合做最基础的 IP 黑名单和频率限制规则引擎还是应该放在能访问业务逻辑的位置。4.4 配置文件与规则库的管理一套良好的配置管理机制比分得更细的规则的更有价值。FckSignups 的配置我用的是 YAML 文件每次调整规则不用改代码重新加载配置就能生效。我的配置文件大致长这样enabled: true log_level: info protocol_check: block_known_clients: false require_browser_ua: false weight: 15 static_rules: disposable_domains_file: ./data/disposable_email_domains.txt keyword_blacklist_file: ./data/signup_keywords.txt weight: 40 honeypot: field_name: company_website weight: 80 behavior_time: min_form_render_seconds: 2 max_form_render_minutes: 30 weight: 25 rate_limit: ip_per_minute: 3 ip_per_day: 10 weight: 70 ip_reputation: datacenter_score: 30 mobile_score: 0 residential_score: 5 decision: review_threshold: 60 block_threshold: 100每个规则的 weight 都独立配置这个设计很重要。上线初期不要急着打开所有规则的封禁能力先把所有规则设为“只记录不执行”跑个两三天看看每条规则如果启用会拦截掉多少真实流量。这样调参时你手上才有真实业务数据而不是靠猜。5. 调优与误杀处理别让真人被当成机器人干掉了5.1 上线第一周最容易踩的三个误杀场景我上线第一周后台的“风险拦截”事件和其他指标相比并不高本来还挺高兴。结果看了几天日志才发现有些真人用户被误拦了。这里说三个最典型的场景如果你也准备部署这几条建议提前预防。第一个场景是公共 IP 误杀。公司、学校这类大出口环境下几十个人可能共享同一个公网 IP某个同事先注册了一个账号剩下的同事在十分钟内再注册就触发了频率限制。这个我之前提过解决方法就是把频率限制的阈值放宽或者用“IP 设备指纹”的组合替代纯 IP 计数。设备指纹可以从浏览器 UA、Canvas 指纹、WebRTC 信息合成虽然不能完全唯一但比纯 IP 精确得多。第二个场景是请求头不完整的真实用户。微信内置浏览器、部分国产安卓浏览器以及安装了一些安全软件的电脑发出的请求头里可能没有sec-fetch-site甚至user-agent都是一串很奇怪的兼容字符串。我在刚开始对“UA 里包含非主流浏览器关键词”的权重和“缺安全头”的权重设得过高这部分用户会频繁命中风险规则。第三个场景是自动化测试和真实用户代理的冲突。如果你们自己有 QA 自动化测试写脚本跑注册流程时用的测试账号密码和注册频率可能跟垃圾注册一模一样。我在上线前把测试 IP 加进白名单了否则每一天的构建任务都在给自己制造误杀事件。5.2 用日志说话每条拦截记录都要能回放FckSignups 的日志一定要记录足够多的上下文。不只是记“某个 IP 被拦了”而是要把这次请求的所有特征快照存下来包含但不限于UA 原文、安全头字段、邮箱域名、触发了哪些规则、各规则得分、请求体大小、表单渲染时间、IP 归属。没有这些快照你发现误杀之后根本无从判断是哪条规则判断失误。我把日志存到独立的日志服务里每天用定时任务跑一遍最近的拦截记录看看有没有可疑的“集中被拦”趋势。比如某个城市的大量用户在同一时段被拦那大概率不是垃圾流量而是规则有偏差。日志留存周期建议至少 30 天一方面方便回溯另一方面也便于给后面的误杀申诉提供依据。如果你有什么防护性的 AI 模型或机器学习模块这批日志也是绝佳的标注样本来源。5.3 降级策略宁可漏掉不可误杀我的原则很简单误杀的代价比漏杀的代价大得多。漏掉一个垃圾账号后面清理的成本顶多是一分钟脚本误杀一个真实用户是实打实的永久流失。所以整个决策链路里所有“高风险判定”一律默认降级为“二次校验”而不是直接拒绝。具体做法是得分达到拦截阈值时不直接返回“注册失败”而是给前端返回一个状态码要求用户完成一个极其简单的辅助验证比如算出随机数学题的结果、拖一次滑块、或者点击一张指定图片。对于真实用户这种操作三秒内完成只是多一步又不至于太打扰对于批量脚本这一步的成本会成倍上升大量脚本根本没有实现这个交互能力自然就放弃了。5.4 配置调整的基本原则调参的时候我有一个习惯永远只动一个变量。如果某天上线后发现误杀率涨了我绝不会同时调两个规则的权重而是对比前后两次日志找出哪个特征的变化直接导致了误杀。这里分享一个统计小技巧看拦截事件中规则命中次数的分布。比如一条规则拦下了 100 次其中 80 次同时命中了另外一条规则那么你调整其中一条时就要意识到另一条也参与了这个拦截决定不能孤立评估单条规则的收益。6. 上线三个月真实复盘数据效果、踩过的坑以及后续扩展方向6.1 我这边获得的数据变化这套网关上线三个月左右效果基本稳定统计数据也足以说明问题。因为每个网站业务差异大具体数值不一定完全适用但趋势可以作为参考指标上线前上线两周后上线三个月后垃圾注册占比约 87%约 23%约 3.8%真实用户平均注册耗时3.4 秒3.6 秒3.9 秒误杀率基于申诉和日志抽检无约 2.1%约 0.3%有效账号占比42%76%95%从 87% 的垃圾注册占比降到 3.8%这个效果对我来说已经非常满意了。真实用户注册耗时增加了零点几秒基本可以忽略不计。误杀率则在持续调优后逐步下降最后稳定在 0.3% 左右其中有两次是因为业务活动叠加了同一出口 IP 的入职潮临时放宽阈值后恢复。6.2 三个写进踩坑记录的教训第一个坑是蜜罐字段名跟业务真实字段撞车。我一开始把蜜罐字段叫fax_number结果线上注册还是涌入了一批垃圾账号。查日志后才发现有些浏览器插件和隐私保护工具会自动填写页面上的表单字段它们把所有 input 都当成“可恢复的输入框”把fax_number自动填充成了一串随机数字。蜜罐防的是机器人结果防到了更“智能”的自动填写工具上。我最终把蜜罐字段改成一个不存在的业务含义且带上 type 属性和 autocomplete 属性。第二个坑是日志没有设置轮转直接把磁盘占满了。网关每天记录的原始请求快照非常大尤其是注册高峰期一个请求的完整 header 快照就有十几 KB一天下来能堆出好几个 GB。上线前我完全忘了做日志清理策略两周后服务报警才发现磁盘空间告急。从那次以后日志全部进入外部日志平台本地只保留三天的临时文件。第三个坑是测试环境的 IP 没加白名单导致测试同事反复被拦截。我们的 QA 同学每天要跑几十遍注册流程结果几乎每一次都被频率限制卡住。后来不得不让网关识别环境变量测试环境直接跳过真实拦截逻辑只保留日志记录。6.3 后续扩展从“防注册”到“防滥用”当垃圾注册问题被压下去之后你会发现“防滥用”这个词的边界更大了。我现在在考虑把 FckSignups 的规则引擎从注册接口迁移到登录、发帖、私信这些更敏感的接口上。套路完全通用只是把输入从“用户名、邮箱”换成“帖文内容、接收人列表”检查逻辑从“是不是垃圾账号”换成“是不是垃圾内容”。尤其是发帖接口图文识别的成本高、延迟大先用规则层挡掉一批纯文本的广告贴效率极高。如果你想自己扩展建议优先把关键词黑名单做成可配置的数据表配合后台管理界面运营人员自己就能往里加词不用每次改代码。另外可以考虑接入简单的机器学习模型对真实用户和机器人的行为序列做分类但这个投入相对大至少等规则引擎稳定、积累了足够样本量以后再做。6.4 个人建议别急着把规则设严先观察再下手我个人最大的体会是反垃圾注册系统的核心不在于“能不能识别垃圾”而在于“能不能在不伤害真实用户的前提下识别垃圾”。一套过于激进的规则哪怕识别率再高如果误杀了一批真实用户整体上也是亏的。所以无论你是直接部署 FckSignups还是自己按这套思路去写一个规则引擎请记住四个字循序渐进。先只读不拦再灰度调整最后再放开执行这永远比一上来就开满火力要稳妥得多。
返回列表