FckSignups:用特征打分与动态监听拦截网页注册弹窗

发布时间:2026/9/16 9:42:56
FckSignups:用特征打分与动态监听拦截网页注册弹窗 你有过这种瞬间吗收藏夹里躺了很久的文章终于打开正文只出现两行剩下的全是登录墙想下载一个工具包点下载按钮被弹到注册页更别提那些等你鼠标刚挪到浏览器顶部就弹出来的订阅框每次都要手动去找那个藏在角落的关闭按钮。最让我爆炸的是自己搭的社区一天能收到几千个注册用户名全是乱码纯纯的脚本灌水。正是在这种状态下我写了 FckSignups。名字糙是糙了点但当时真的签到烦了——与其忍不如直接干掉它。FckSignups 本质上是一个跑在浏览器里的用户脚本用“特征识别 动态 DOM 监听 动作分发”的方式把网页里那些“不注册不让你好好看”的弹窗、注册表单、遮罩层自动移除或拦截同一套打分思路稍微改改还能用到服务端做反垃圾注册过滤。下面会给出完整的可运行代码、特征权重表、踩坑记录和调试方法适合被垃圾注册困扰的站点管理员也适合单纯想在浏览网页时少被打扰的普通用户。1. 先看清敌人四种典型的注册骚扰1.1 注册墙内容被账号锁死打开文章只显示开头两段随后全屏遮罩按钮写着“注册账号继续阅读”。这类模式在资讯站和文档站最常见。关键特征全屏或半屏的 overlay、body 的 overflow 被改成 hidden、以及一个包含邮箱和密码的表单。对这类FckSignups 的处理是检测到表单打分超过阈值后向上找最近的 modal/popup/overlay 容器直接移除并把 body 滚动恢复。注意不是简单地 display:none而是 remove()把节点从 DOM 里彻底干掉避免那些在关闭按钮上挂状态监听的前端代码继续生效。1.2 订阅弹窗等你鼠标挪出页面再出手订阅新闻信newsletter弹窗通常是“离开意图”触发你的鼠标刚移到浏览器顶部系统判定你要关页面瞬间弹出一个“留下你的邮箱吧”。这类弹窗是后来注入的 DOM初始 HTML 里没有所以暴力清除脚本如果只在页面加载时扫一遍3 秒后弹窗插进来扫描早就结束了。这就是为什么必须用 MutationObserver 持续盯着页面变化等弹窗节点插进来的第一时间就去处理。1.3 伪装成下载/查看的注册引导“点击下载”按钮实际是个 a 链接跳转到注册页面也有“阅读全文”按钮进去以后还是注册。对于这类FckSignups 里有个特殊的 redirect 模式如果链接的 href 指到 signup/register/join 等路径脚本可以阻止默认跳转并尝试回退到原文链接。但下面这条边界非常重要如果资源本身必须登录才能下载这是站点的规则脚本不去伪造会话、不去绕服务端鉴权只处理前端的“诱导层”。比如下载按钮被引导到一个纯注册页而资源其实公开可用这种场景可以救回来如果后端真正做了权限控制前端再怎么拦都白搭那就不该和站点规则硬刚。1.4 账号农场不是打扰你是污染你的数据库还有一类“注册骚扰”不是面向普通浏览者的而是冲着你服务器的注册接口来的。机器人用随机邮箱和用户名批量创建账号主要用来发广告、刷评论、刷权重。解决思路跟前端拦截完全两回事得在服务端接一层过滤器用蜜罐字段、提交耗时、邮箱域名黑名单、频率限制去挡住。这个我在第 4 节单独讲虽然和 FckSignups 名字同源但它真正的位置在你的后端中间件里。2. 怎么判断一个表单到底该不该拦2.1 无脑删表单一定会翻车写第一版的时候图省事写的是“只要页面上出现 form 就全删”。结果嘛第一个真实页面就把登录框干掉了用户想登录发现表单没了直接懵。原因很简单登录框也是 email password也是 form表面上看和注册框长得一模一样。如果按“有邮箱有密码就删”的规则那全站都不用登录了。因此必须给表单做特征打分不是非黑即白而是算出一个分数超过阈值才动手。2.2 特征打分表我的实现里分数是加权叠加的。下表是核心特征和权重实际用的时候可以根据你经常访问的站点类型调整特征权值说明action、class 或 id 含 signup / register / join / subscribe4强信号直接标明注册意图表单可见文本含“注册 / 创建账号 / 立即加入 / 免费订阅”2中文站点上的强信号同时存在 email 输入框和密码输入框2注册和登录都会命中只能算中等信号存在 autocompletenew-password2新密码提示注册场景的标志存在同意条款 checkbox1注册流程常要求勾选协议存在“忘记密码”链接或 autocompletecurrent-password-3登录/找回特征直接降权action 等属性含 login / signin-2明确是登录不该拦阈值我默认设成 8。有些新闻站订阅框打得非常准邮箱 订阅按钮 大遮罩分数轻松过 10而登录框通常会被“忘记密码”和 current-password 拉到 2~3 分稳稳放行。2.3 MutationObserver等弹窗自己冒出来很多注册弹窗不是页面初始就有的而是在定时器或“离开意图”触发后才注入到 DOM。所以脚本不能只跑一遍得持续观察页面变化。MutationObserver 是浏览器原生 API性能比 setInterval 轮询好很多去监听 childList 和 subtree有节点插入就重新扫描一次。但要注意一插就是几十个节点如果每次都全量扫描所有 form 会有点浪费因此我外面包了一层 300ms 的防抖把同一批插入合并成一次处理。3. 核心代码拆解FckSignups 的拦截器3.1 脚本主入口先上是完整可用的一版Tampermonkey / Greasemonkey 都能跑// UserScript // name FckSignups // namespace local.fcksignups // version 0.4.2 // description 移除并拦截网页里的注册弹窗和注册表单保留登录/找回密码 // match http://*/* // match https://*/* // grant none // run-at document-idle // /UserScript (() { use strict; const CFG { threshold: 8, debug: true, whitelist: [github.com, news.ycombinator.com], }; function isWhitelisted(host) { return CFG.whitelist.some(w host w || host.endsWith(. w)); } function scoreForm(form) { let score 0; const attrs [ form.getAttribute(action) || , form.id || , form.className || ].join( ).toLowerCase(); const text (form.textContent || ).toLowerCase(); [signup, sign-up, register, create-account, join, subscribe, newsletter, member] .forEach(w { if (attrs.includes(w)) score 4; }); [注册, 创建账号, 立即加入, 免费订阅, 加入] .forEach(w { if (text.includes(w)) score 2; }); const email form.querySelector(input[typeemail], input[name*email i]); const pass form.querySelector(input[typepassword]); const newPass form.querySelector(input[autocompletenew-password]); const currentPass form.querySelector(input[autocompletecurrent-password]); if (email pass) score 2; if (newPass) score 2; if (email !pass) score 1; if (form.querySelector(input[typecheckbox])) score 1; if (currentPass || form.querySelector(a[href*forgot], a[href*reset])) score - 3; [login, signin, log-in, sign-in].forEach(w { if (attrs.includes(w)) score - 2; }); return score; } function findOverlay(el) { return el.closest(div[id*modal i], div[class*modal i], div[class*popup i], div[class*overlay i], div[class*lightbox i], div[roledialog]); } function restoreScroll() { document.documentElement.style.overflow ; document.body.style.overflow ; } function actor(form) { if (isWhitelisted(location.hostname)) return; const score scoreForm(form); if (CFG.debug) console.info([FckSignups] form scored, score, form.action || form.id); if (score CFG.threshold) return; const overlay findOverlay(form); if (overlay) { overlay.remove(); restoreScroll(); return; } form.addEventListener(submit, ev { ev.preventDefault(); ev.stopPropagation(); console.warn([FckSignups] blocked signup submit); }, true); } function scan() { document.querySelectorAll(form).forEach(actor); } function debounce(fn, wait) { let t; return (...args) { clearTimeout(t); t setTimeout(() fn(...args), wait); }; } const scanner debounce(scan, 300); new MutationObserver(scanner).observe(document.documentElement, { childList: true, subtree: true, }); scan(); })();3.2 打分函数为什么这么写核心是 scoreForm。它把所有“文案特征”和“行为特征”捏在一起算一个总分而不是用单个条件做决定。这里有个细节判断邮箱输入框时用了[name*email i]带上 i 标志做不区分大小写匹配。很多站点的输入框 name 写的是 Email 或 EMAIL前两版脚本就是栽在这里全小写匹配会漏掉。还有一点登录框和注册框都可能有 email password只靠这个判断一定误杀。所以代码里特别检查了 current-password。按 HTML 规范登录表单的密码框应该是autocompletecurrent-password注册/改密表单才是new-password。现在主流框架基本都按这个标准写靠着它可以把登录框稳稳降权。3.3 三种动作怎么选remove表单在弹窗里整颗弹窗节点直接 remove顺手恢复 body 滚动。前提是 findOverlay 能通过 id/class 匹配到外层容器。匹配范围故意写得宽一些涵盖 modal、popup、overlay、lightbox、roledialog但用带 i 的不区分大小写匹配避免漏掉Modal这种写法。block表单是页面内嵌的、找不到外层弹窗容器时不删节点而是给 submit 绑定捕获阶段的监听阻止提交。为什么用第三个参数 true因为这样可以抢在页面自己的 submit 处理之前拦截尤其在事件里同时调用了 stopPropagation 的时候能第一时间断掉后续提交链路。skip第 1.3 节提到的“跳转到注册页”的链接在完整版里可以同时监听 a[href] 的 click检测 href 是否命中 signup/register/join 类路径再决定是否放行。这属于配置型能力默认关闭避免误伤正常注册入口。4. 同一套思路搬到服务端反垃圾注册过滤器4.1 蜜罐字段原理往注册表单里塞一个用户绝对看不到、正常用户也不会填的隐藏字段比如“公司网站”或“确认邮箱”用绝对定位移出可视区同时加 tabindex-1、aria-hiddentrue 防止被辅助技术读到。真人会忽略它机器人会无脑把每个字段都填了。服务端收到请求后只要发现这个字段有值直接判定机器人不发验证邮件、不写库静默拒绝。为什么不能简单用 display:none因为不少机器人只识别“可见”字段display:none 的字段反而会被跳过用移出视口的方案在视觉和语义上都是隐藏的但字段真实存在于表单里机器人反而更容易踩中。4.2 邮箱域名黑名单垃圾注册最常见的模式是使用一次性邮箱域名。社区站点可以维护一个名单在注册接口里校验邮箱后缀命中就拒绝或转人工。但要提醒的是硬拒绝会误伤一部分用隐私邮箱的真实用户。我的做法是命中黑名单时不直接 403而是提示“为了安全该邮箱需要额外验证”发送包含动态链接的确认邮件把门槛提高但不彻底关死。4.3 时间陷阱人在表单里正常填邮箱、密码、点击同意协议哪怕手速再快也得 3 秒以上机器人提交往往在 1 秒内。做法前端在表单渲染时把一个毫秒时间戳存进隐藏字段服务端拿当前时间减掉这个值小于 2~3 秒的请求直接打回。这里有个坑时间戳必须是表单 render 时生成不能是页面 load 时生成否则用户挂机 10 分钟再提交时间差会被误判成机器人。更稳妥的做法是同时记录前端三个里程碑渲染、首次输入、提交三个差值一起看。4.4 频率限制的平衡点对同一 IP 或同一邮箱做滑动窗口限流比如 60 分钟内最多 3 次注册。超过就要求走 CAPTCHA 或邮箱验证。太严格会误伤公司局域网、校园网这类出口 IP 共享的场景太松又拦不住批量脚本。折中的做法是注册接口本身就严格要求蜜罐 时间陷阱限流只作为最后一道兜底阈值可以放宽到 10 次/小时。5. 实战踩坑误杀登录框、SPA、shadow DOM5.1 误删登录表单的翻车现场第一版上线当天就收到反馈某知识社区用户点登录弹窗闪了一下就消失了。排查后发现那站点登录弹窗的容器 class 竟然是register-wrap这是历史遗留样式我们的打分函数看到 register 直接加了 4 分再加上 emailpassword 的 2 分总分过了阈值整颗弹窗被移除。修复方式就是加 negative 特征登录框里的“忘记密码”链接和current-password自动补全必须能压过注册词。现在规则是先算加分再算减分总分为负的不动这是调试日志里最先要看的指标。5.2 SPA 路由切换之后为什么不灵了React、Vue 这类单页应用可以只替换页面局部 DOM不像传统页面整页刷新。理论上 MutationObserver 挂着就能兜底但实际会碰到两个麻烦一是路由切换瞬间的频繁 mutation 会导致防抖扫描被不断推迟弹窗在扫描间隔里出现又消失就被跳过了二是很多团队把弹窗组件做了懒加载组件 chunk 加载完成后才在 rAF 里插入 DOM如果你的扫描下一次执行正好在 chunk 加载之前就会漏掉这一次直到弹窗再次被触发。所以除了 observer还要在路由变化后主动 scan 一次作为补位监听 popstate 和 hashchange必要时 monkey-patch 一下 history.pushState。5.3 shadow DOM 和 iframe 里的表单普通 querySelectorAll 根本穿不进 shadow DOM。现在不少前端组件库把弹窗封装在自定义元素里外层的 document 选择器看不到里面的 form。解决方法是递归遍历 open shadow rootfunction scanIn(root document) { root.querySelectorAll(form).forEach(actor); root.querySelectorAll(*).forEach(el { if (el.shadowRoot) scanIn(el.shadowRoot); }); }iframe 则是另一层限制。加脚本元数据里的all-frames可以覆盖同源 iframe跨域 iframe 由于安全协议无法从外部操作这种情况就交给各自 iframe 内部运行的用户脚本处理。5.4 调试技巧先开日志再开火力敏感操作最忌讳一上来就是 remove。我的建议是首次使用把 CFG.debug 设为 true脚本只打分、只打日志不删除、不拦截跑一周。浏览器控制台里会留下所有表单的 score把那些“你本来要正常登录”却被打了高分的表单记录下来调完权重再切换成拦截模式。这种方式比反复上线测试版效率高很多因为你同时看到了正反两面的证据。6. 安装、配置和边界6.1 三种使用方式最标准的是 Tampermonkey 扩展新建脚本 → 粘贴上面的代码 → 保存 → 刷新页面。手机上可以装 Firefox Tampermonkey或者 Kiwi 浏览器 Tampermonkey效果一致。如果你只是临时想在某个页面试一下打开开发者工具 Console把去掉 UserScript 元信息的主逻辑贴进去也能跑缺点是刷新就没了。Whitelist 配置要主动用。像 GitHub、Stack Overflow、语雀这类我本来就登录并信任的站点直接加入 whitelist 跳过全部检查既省性能也避免误伤。默认配置里已经放了一部分按自己常访问的站点继续加。6.2 强烈不建议用的场景银行、政务、医疗这类有实名和合规要求的平台注册流程不只是流程更是法定义务。脚本的粗暴删除可能让你漏掉重要的服务条款、实名认证提示甚至影响后续密码找回。真遇到这类站点把域名加进 whitelist 或者干脆别装脚本。另外再次强调FckSignups 处理的是“打扰型”注册弹窗和垃圾注册不是破解工具。站点如果靠服务端权限控制强制登录前端删掉表单只会让你什么都点不了没有任何收益还违反网站的使用协议。6.3 后续能扩展的方向如果这个项目真的被更多人用起来我打算把特征库做成可订阅的规则包按“资讯站”“文档站”“社区站”分类大家直接导入别人调好的权重再用 GM_setValue 做一个简易设置面板阈值、白名单、动作模式都能在扩展菜单里改不用每次编辑代码。服务端过滤器那边也可以抽成一个中间件包让 Nginx 或 Express 应用一条指令接入。装完脚本用到现在大概三周。最大的体会不是“弹窗少了”而是我重新拥有了对页面的控制权——我想注册才注册而不是被网站逼着注册。开发 FckSignups 的过程也让我养成一个习惯凡是自动执行的东西一定要有日志模式先观察再下手。如果你准备照这套思路做一个类似工具我的建议很简单第一版绝不删除只记录把阈值和特征权重调稳了再打开真正的暴力开关。就这个节奏能帮你省下后面至少两轮误杀的麻烦。