
FckSignups——这名字第一次出现在我收藏夹里时我确实在工位上笑出了声。把脏话脱敏再塞进项目名暴躁又克制像极了被垃圾注册逼疯的深夜开发者。但笑完之后我意识到它背后是一个真实且昂贵的麻烦每一家面向C端的SaaS、社区、电商甚至内容平台都在被同一件事消耗着——注册环节的垃圾流量。这个项目解决的是注册流程中的恶意注册、批量垃圾账号和机器人滥用它的核心不是让你“再画一次验证码”而是一整套可以自托管、可调参、有审计的注册风控网关。以下是我把FckSignups从概念做到落地的完整记录。1. 被垃圾注册逼疯的“增长事故”1.1 一场让我心态爆炸的Beta测试事情得从我的一个社区类小项目说起。Beta测试第一周用户注册量突然冲到了每天6000多次团队一度以为是产品火了。结果第二天晚上运维拉了个表看到数据的那一刻大家都沉默了75%的新账号用的是同一个临时邮箱域名昵称是随机字符拼接头像全是相同尺寸的纯色方块。更要命的是这批账号注册进来之后立刻开始发广告、刷评论、发站内信。整个社区的新内容区里前50条帖子里有43条来自这些假账号。我们花了整整三天清理数据、封号、加验证码、改注册接口但第二天攻击者换个IP池和邮箱域名又进来了一批。那是第一次我意识到注册保护这件事绝不是“加个验证码”就能糊弄过去的。这种经历估计做SaaS或内容产品的朋友都有共鸣垃圾注册不只是让用户增长数据变难看它还会实实在在烧掉你的短信费、邮件服务费、对象存储费以及团队最宝贵的时间。更严重的是当真实用户进入社区看到满屏垃圾内容时他们不会觉得是攻击者的错只会觉得“这个产品不行”。1.2 FckSignups到底是个什么定位FckSignups这个项目的本质是做一个自托管、轻量级、可灵活调参的注册风控网关。它的定位非常明确放在你的注册服务前面像智能门卫一样对每一个注册请求做实时评估输出四种结果Allow放行正常走注册流程。Challenge进入二次验证比如滑块、邮箱验证码、短信验证码。Block直接拒绝并记录原因。Review放进人工审核队列或异步标记观察。它会保存完整的证据链这个请求来自什么IP、用什么设备、是否有自动化特征、该IP在过去一段时间注册了多少次、邮箱域名是否常见的一次性邮箱等等。我倾向于把它理解成“注册环节的风控中间件”而不是某个单一的验证码组件。传统人机验证解决的是“你是不是真人”而FckSignups更关心的是“你这个真人来注册到底是不是带着善意来的”。这个视角上的差异就是它和普通验证码最大的不同。2. 选型对比现成验证码方案和自研风控的边界2.1 现成验证码方案的四个“管不住”先声明一下reCAPTCHA、hCaptcha、Turnstile这些方案我全都认真用过它们对大规模纯脚本机器人的识别效果确实很强。但为什么我在FckSignups里还是决定自己搭一套因为在实际业务里单纯靠验证码会碰到几个很尴尬的场景。第一个是真人众包。这是攻击者现在最常用的手段把注册任务分发给大量兼职人员由真人手动完成注册。真人手动注册时验证码是能通过的行为轨迹也是正常的所以验证码在这里基本失效。第二个是临时邮箱和接码手机号。攻击者通过临时邮箱域名、虚拟号段批量接收验证码验证码流程走完之后拿到的还是垃圾账号。验证码只能证明“邮箱/手机号是能收码的”但证明不了“这个身份是用户的长期资产”。第三个是黑盒评分与业务规则的矛盾。第三方验证码服务通常会返回一个风险评分但它是黑盒的你只能凭经验手动定一个阈值没法精确到“这个IP一小时内注册超过5次就拦截”这种颗粒度。第四个是数据出境和合规问题。设备指纹、IP、浏览器信息这些信号属于敏感个人信息如果业务对数据主权有要求把这类数据实时发送到第三方服务会带来额外的合规风险。2.2 自研风控的选型决策我做一个简单的选型对比这也是我一开始纠结过的路径方案优势短板适用场景reCAPTCHA自动化识别能力强Google生态完善部分地区访问体验不稳定黑盒评分隐私敏感面向海外用户的轻量防护hCaptcha隐私友好可定制难度用户完成率受交互影响仍难防真人众包重视隐私合规的C端站点Cloudflare Turnstile无感验证体验好运维简单规则定制能力有限策略依赖平台需要快速上线且不想自运维自研风控网关数据闭环规则完全可控可结合业务需要持续维护威胁情报和调参有多层防滥用诉求的产品团队最终FckSignups的技术选型是这么定的后端核心用Go编译成单个二进制部署太省心了性能也足够高并发下不会成为瓶颈。规则引擎用YAML配置文件不是为了炫技而是为了“运营人员也能改策略”。风控策略需要频繁调整如果每次改阈值都要走代码发版等改完垃圾账号早就刷完一轮了。前端SDK用原生JavaScript无依赖SDK体积控制在15KB左右gzip后不依赖第三方框架只要能跑浏览器的地方就能接入。数据存储先用Redis做计数和滑窗统计结果异步落库注册接口的时效性要求很高不能因为风控多出几百毫秒的延迟所以实时决策走内存或Redis审计数据异步写数据库。这套组合配合起来单次风控评估在服务端的耗时基本能压在10-20毫秒前端采集的开销约8-15毫秒用户几乎感知不到。3. 三层防线设计从秒级拦截到慢鸟式深度审计3.1 第一层实时决策里用到的风险信号与评分逻辑第一层防线是注册请求进来的瞬间做的一次快速评估。它要回答的问题是这个请求本身是不是已经足够可疑可以直接拒绝。我最初实现的信号集是这样的信号说明典型权重IP信誉分该IP历史上是否频繁注册、是否命中机房IP段0-40注册频率同一IP/同一设备指纹在滑动窗口内的注册次数0-40邮箱域名风险是否命中一次性邮箱域名库0-25Headless浏览器特征是否命中无头浏览器的JS特征0-50设备指纹完整性Canvas/WebGL/UA是否缺失或不一致0-20风险信号缺失前端SDK埋点没返回行为数据10风险评分是简单的加权累加超过70分直接Block45到70分之间进Challenge流程。有人可能会问为什么不用机器学习模型因为规则引擎在这类反滥用场景里有几个不可替代的优势第一解释性强运营和客服看得到“为什么被拦”第二冷启动没问题不需要积累大量标注样本第三规则调整是秒级生效的模型迭代则需要一套复杂的流程。机器学习不是不能用而是等业务体量到了“每天几百万注册”再考虑也来得及。关键逻辑我用伪代码表达一下func EvaluateSignup(req SignupRequest, risk RiskSignals) Decision { score : 0.0 if risk.DisposableEmail { score 25 } score ipReputation.Score(risk.IP) // 0-40 score rateLimiter.Increment(risk.IP, now) // 滑动窗口内注册过多则加分 if risk.HeadlessDetected { score 50 } if risk.DeviceFingerprintMissing { score 20 } if risk.BehaviorDataMissing { score 10 } switch { case score 70: return Decision{Action: block, Reason: score_high, Score: score} case score 45: return Decision{Action: challenge, Reason: score_mid, Score: score} default: return Decision{Action: allow, Reason: score_low, Score: score} } }这个评分可以结合自家业务继续加信号。比如电商加“收货地址风险分”社区加“昵称随机度检测”活动平台加“设备数量限制”。规则越多覆盖越广但同时也要注意别把规则加得太庞杂否则后面排查问题会很痛苦。3.2 第二层行为特征与设备指纹为什么难绕第二层防线是“Challenge”阶段。当一个请求进入中间分区间时FckSignups不会直接拒绝而是要求它通过一次额外的验证挑战。但挑战不只是给用户添麻烦它同时是一次“行为采集”的机会。前端SDK在用户打开注册页的那一刻就开始工作了采集四类数据鼠标轨迹真人移动鼠标时有加速度、有抖动、有回环修正程序生成的轨迹往往是直线或匀速运动。键盘节奏真人在输入邮箱和密码时会有停顿、删除重打、不同键位间的间隔脚本模拟的输入节奏过于均匀。页面停留与滚动真人注册通常会在页面停留几秒到几十秒滚动查看条款纯自动脚本几乎不会做这些动作。Canvas/WebGL指纹利用不同设备渲染结果存在差异的特性生成一个设备指纹。同一台设备的指纹在正常情况下是稳定的。其中Canvas指纹可以稍微展开讲一下。原理是让浏览器用Canvas API绘制一段固定文本或图形然后提取像素数据的哈希值。由于不同设备的显卡驱动、字体渲染、抗锯齿算法存在差异哈希值会不同因此能够作为设备辨识依据。头部浏览器环境里由于缺少字体和GPU加速往往生成的Canvas指纹不稳定这本身就是一种机器人特征。这套机制的一个聪明之处在于它让“攻击者需要模仿的东西”变多了。以前绕过验证码只需要让脚本能识别图片、能提交表单现在则要让脚本去模拟一套完整的“手部操作”和“设备环境”。模拟鼠标轨迹的难度不高但要把节奏模拟得和真人一样成本就上来了。攻击者每花一分力气去模仿真实行为你作为防守方就多一分胜算。3.3 第三层异步团伙检测与申诉机制前两层是用户能感知到的防线第三层则发生在背后。FckSignups会把每次注册事件异步写入审计队列离线任务会定期做“关联图谱分析”。做法不复杂以设备指纹、注册IP、浏览器UA、邮箱域名、手机号段这些字段作为关联节点找出“共享同一批设备指纹但IP分布在多个地区的账号”或者“同一手机号段集中注册的账号”。这类账号往往是同一个团伙在操作可以通过并查集或社区发现算法把它们批量识别出来然后统一封禁或降权。为什么需要这一层因为很多攻击者已经学会“低强度攻击”一个IP一天只注册两三次行为特征也模拟得很好单独看每个请求都是正常的。只有把时间拉长、把多个请求关联起来看才能发现“这批账号是同一个设备指纹产生的”。这层还要配套一个用户申诉和人工复核渠道。风控策略再精准也会误杀申诉流程不是摆设它是挽回真实用户的关键一步。我给FckSignups加了申诉接口和被拦截用户自助解锁功能用户提交申诉后系统会自动比对证据链如果判断存疑就直接转人工审核。上线以来通过申诉恢复的正常用户占比虽然不高但这些人往往是产品最活跃的核心用户挽留他们带来的长期价值非常大。4. 一条龙接入实录从埋点到生产4.1 接入前优先想清楚的三件事很多团队接入风控上来就打开文档改代码结果上线一星期就骂骂咧咧地回滚。我踩过这个坑之后现在会先让团队想清楚三件事。第一什么样的注册行为在业务里叫“可疑”。比如一个内容社区正常用户一天注册1个账号就够了那么“同一IP一天注册超过5个”就是可疑但如果是某个抽奖活动用户可能用多个小号抽奖阈值就完全不同。这个定义要产品、运营和安全一起坐下来聊不能只由开发拍脑袋。第二Block之后的用户页面长什么样。直接拒绝会给真实用户留下“这个产品在赶我走”的坏印象所以Block页面要尽可能温和甚至可以引导用户联系客服申诉。Challenge页面则要考虑不同设备的兼容性不能让用户卡在一个永远过不去的滑块上。第三日志和审计要留够证据链。每次拦截请求至少要记录风险评分、命中的规则、关键信号值、决策时间和原始请求数据。没有这些证据后面处理用户申诉和复盘攻击手法时会非常被动。4.2 核心配置文件和接入步骤这是FckSignups的YAML配置示例几乎就是接入的全部核心server: listen: :8080 mode: release redis: addr: 127.0.0.1:6379 rateLimit: window: 1h maxPerIp: 10 maxPerDevice: 5 signals: disposableEmailEnabled: true deviceFingerprintRequire: true behaviorDataRequire: true headlessDetectEnabled: true thresholds: challenge: 45 block: 70 challenge: mode: redirect redirectUrl: /verify接入步骤拆开看是这样的部署服务把Go二进制扔到服务器上配上Redis地址跑起来。这一步基本没有依赖问题唯一的坑是Redis的滑动窗口计数结构依赖系统时钟记得把服务器时间同步打开NTP。配置规则根据上一步定义好的“可疑”标准填上阈值和规则。初期阈值建议放松一点宁可多放一些可疑账号进来Review也不要误杀真实用户。接入前端SDK在注册页引入fcksignups-sdk.js初始化后它会自动采集行为数据和设备指纹并在表单提交前生成一个风控Token放进隐藏字段里一起提交。// fcksignups-sdk.js 初始化示例 FCKSignups.init({ serverUrl: https://risk.example.com, tenant: my_biz, fields: { emailInput: #email, passwordInput: #password } });接入后端接口注册接口收到请求后把风控Token连同业务参数一起交给FckSignups做评估根据决策结果走不同分支。curl -X POST https://risk.example.com/v1/evaluate \ -H Content-Type: application/json \ -H X-Tenant: my_biz \ -d { token: 前端生成的force_token, ip: 1.2.3.4, email_domain: guerrillamail.com, register_event_id: evt_123 }服务端返回大概是这样的{ decision: block, score: 82, matched_rules: [ disposable_email, high_ip_registration_frequency ], action_url: https://example.com/blocked }配置管理后台登录Dashboard查拦截日志、看实时数据、调规则阈值。这一步一定不要省否则线上出了误杀你只能靠用户骂你才后知后觉。4.3 上线后的真实效果与调参记录我在那个社区项目里接上FckSignups之后记录了三个阶段的数据阶段垃圾注册占比真实用户误杀率平均服务端耗时接入前图形验证码18.2%-约80ms第一版IP限流临时邮箱黑名单Headless检测4.3%2.1%约15ms加入行为特征设备指纹后0.7%0.4%约18ms误杀率从第一版的2.1%降到0.4%主要靠两件事一是把“Headless检测”从“判定即拦截”改成了“加分项”避免过度依赖单一信号二是给明显正常的请求开了“白名单快通道”比如来自主流邮箱服务商的请求基础分数直接减一部分。调参过程最有价值的经验是上线前先跑两周纯日志模式只记录决策结果但不真正拦截任何请求。这样能拿到足够多的真实业务样本去做阈值校准而不是刚上线就误杀一片。很多团队急着把风控开成“拦截模式”结果第二天用户投诉量爆表这种现象完全可以靠纯日志模式来避免。5. 踩坑实录误杀、漏杀和调参路上的真实事故5.1 误杀案例隐私模式用户与指纹漂移上线没多久就接到一个用户投诉在公司电脑上用“无痕模式”打开注册页结果直接被Block了。排查日志后发现原因是他打开了隐私模式Canvas指纹在这台机器上只生成了部分数据加上WebGL信息被浏览器屏蔽设备指纹完整性得分直接拉满。这个问题现在几乎每条写风控的文章都会提醒但真到自己调试时还是会忽略。设备指纹这个信号很强大但它有个天然缺陷隐私模式、浏览器升级、字体渲染变化都会导致指纹变化。应对办法是在代码里给“指纹不完整”一个单独的中等权重而不是灾难性地判成高风险。同时把“指纹不完整”和“高频注册”连在一起看才有意义如果只是指纹不完整但IP信誉良好就完全不用拦。5.2 误杀案例Headless检测的“正确正判”错误有一版规则上线后我们误杀了一批真实用户。查了半天发现是检测逻辑里有一项“navigator.webdriver true”的判断。这个属性在自动化浏览器里正常会被设为true但有极少数用户浏览器扩展或代理工具也会把这个属性渲染成true导致这些真实用户被识别成Headless。这也引出一个重要原则自动化检测特征不要单独作为“硬拦截”信号只能作为评分项。比如检测到webdriver标记加30分检测到plugins数量为0再加20分直到累积到阈值才触发拦截。单独看任何一个指标都会误伤。5.3 漏杀案例真人众包不是技术问题有一次攻击者换了策略不再用批量脚本而是把注册任务发给了真人兼职群。那波攻击我们几乎完全没拦住因为每个请求都是真人在浏览器上操作的行为特征、设备指纹、IP都正常。唯一可疑的是这批账号在注册后的行为模式高度一致统一在注册后3分钟修改头像、发同一篇推广帖。事后复盘我意识到当攻击成本高到一定程度反滥用就不只是技术问题了而是成本博弈。对这类真人众包攻击技术上的应对手段有限但也不是完全没招可以监控新账号的后续行为模式发现“群组化行为”后整批降权也可以提高新账号的权限门槛比如注册24小时内不能发帖、评论需要完成手机号验证等。这些业务策略配合技术手段才能把攻击成本抬到攻击者无法接受的程度。5.4 临时邮箱名单的时效性坑临时邮箱域名库是FckSignups里最基础、也最需要维护的一块数据。它的坑在于攻击者会用程序批量生成新域名你今天拉黑的一个域名下周可能已经被替换了。所以“静态域名黑名单”只能拦截最懒的攻击者真正要效果好必须定期从多个渠道更新域名列表或者按“域名存活时长”做动态判断——很多临时邮箱域名的创建时间很短通过WHOIS信息可以识别出来。5.5 规则热更新比“改代码发版”重要得多最初我把风控阈值写死在Go代码里每次调参都要改代码、走CI、发板过程冗长到让人崩溃。有一次发现误杀率飙升等我把代码改完发布完已经过去两小时了期间至少丢了上百个真实用户。后来我改成把规则放到YAML配置里通过Dashboard实时下发更新Redis里存当前生效的规则版本。现在调整阈值只需要在后台改几个数字15秒内生效整个团队再也不用为了调个阈值拉着研发开会。5.6 日志留证与申诉闭环还有一件事值得单独列出来风控日志一定要完整可回溯。我见过有些团队只记录一个“pass/fail”结果被拦截用户申诉时客服完全不知道发生了什么只能放行或拒绝。FckSignups的每一条审计日志都包含风险评分、命中的规则、原始信号快照。这样处理申诉时客服可以直接看到“因为您的邮箱域名命中临时邮箱库且该IP一小时内注册超过10次所以被拦截”沟通成本瞬间降下来。申诉通过后系统会自动把相关IP或指纹加入白名单一段时间避免同一用户二次被误杀。6. 长期主义反垃圾注册不能做成一次性工程6.1 反滥用系统的本质是猫鼠游戏很多团队把反垃圾注册当成“上线一次就解千愁”的项目这是最大的误区。攻击手法在持续演进今天用临时邮箱明天用接码手机号后天用真人众包再往后可能直接用AI驱动浏览器流程。你的风控系统必须有一套持续运营机制而不是上线后扔在那自生自灭。在FckSignups的运营中我把维护工作重点放在三张表上IP信誉表从历史注册日志中积累每个IP的注册行为画像识别机房间段和高频注册段这个表每周更新一次。临时邮箱域名表每天定时拉取公开的黑名单源加上自己从拦截日志里提取的新域名写入动态黑名单。设备指纹观察表记录异常设备指纹的历史行为比如某个指纹在24小时内关联太多账号就会被观察。这三张表不能靠人工维护要用脚本自动更新人工只负责定期审查更新质量。否则第二天起床就会看到垃圾注册率反弹。6.2 用指标监控代替“凭感觉调参”风控系统上线后至少要看住四个指标注册转化率、拦截率、误杀率、挑战通过率。前两个反映“风控严不严”后两个反映“风控准不准”。我们的监控告警规则是这样的误杀率超过0.5%就触发告警挑战通过率低于85%也要告警因为可能是Challenge流程的产品体验出问题了。有一次正是靠这个告警在活动大促开始前发现新上线的滑块组件在低端Android机型上渲染不出来才避免了活动当天大量真实用户被卡死在验证环节。另外新策略上线时尽量走灰度先对5%的流量生效观察半小时数据确认误杀率没有异常后再逐步放量到50%、100%。这个流程看似繁琐但在超大注册量场景下一次误判就可能引发舆论事件。6.3 业务协同当技术方案遇到天花板前面提到真人众包攻击时已经触及了纯技术方案的天花板。要突破这个天花板必须把技术与业务策略协同起来。几个亲测有效的手段新账号冷启动降权比如新注册账号在24小时内不能发长文、不能私信。即使攻击者注册了大量账号短期内也无法产生破坏力。邀请码/信任值体系老用户邀请的新账号可以获得更高的初始信任值减少被风控误伤的概率而没有受信来源的注册则要走更严格的验证。手机号资费验证在活动场景下对高价值权益用短信验证流程增加成本。攻击者接码一个手机号的成本可能是几毛钱到几块钱如果羊毛带来的收益低于这个成本他们就会放弃。工商信息与实名信息的延伸金融、合规行业可以叠加实名认证进一步拉高伪造身份的成本。这些手段的本质都是“增加攻击成本”。当攻击成本接近甚至超过攻击收益时垃圾注册自然就停了。6.4 合规与隐私设备指纹不是免检品最后这一点我建议所有做风控的人都重视起来。设备指纹、IP地址、行为轨迹数据在很多监管体系下都属于个人信息的范畴。如果你的产品面向未成年人或敏感行业用户处理这类数据还会有额外限制。我的经验是风控响应必须满足三条基本原则——最小必要原则只采集风控所需的信号不多采集、透明告知原则隐私政策里写清楚采集内容和用途、访问与删除原则要给用户提供查询和删除自己风控数据的渠道。因为FckSignups是自托管方案数据从头到尾都在自己的服务器上不经过第三方这本身就是合规上的一个利好。但自托管不等于可以随便采集该做的合规动作一样都不能少。7. 如果重来一次我会用哪条路线快速落地如果现在让我从头再做一个新产品的注册风控我不会一上来就把FckSignups全套功能都堆上。先跑通一条最基础的路线就足够了第一周只上第一层防线IP限流、临时邮箱域名黑名单、Headless检测三个信号跑纯日志模式观察数据不做拦截。第二周根据日志调整阈值开启Challenge模式让可疑流量走滑块或邮箱验证。第三周确认误杀率稳定在1%以下后再接入行为特征和设备指纹把评分体系从“简单信号”升级成“组合信号”。这条路线的好处是每走一步都有数据支撑不会出现“一上线就误杀”的翻车事故。等跑通了基础再逐步把异步团伙检测、规则热更新这些进阶能力加进去。我个人在实际项目中最大的体会是反垃圾注册不是一个“搭完就结束”的功能而是一个需要长期运营、持续博弈的过程。FckSignups这个项目里我最满意的一点不是它用了多高深的算法而是它把“注册保护”这件事变成了一个可观测、可调整、可解释的系统。哪怕被攻击者绕过了某一条规则你也可以从日志里快速找到漏洞、改掉规则、在下一次攻击来临前堵住缺口——这种掌控感是我觉得自研风控最值得的地方。