第 11 章:防薅羊毛
真实遇到的两类攻击
这些经验来自 SaaS 项目运营过程中,遇到了才去解决。
第一类:自动化脚本批量注册
短时间内大量临时邮箱域名批量注册。特点是验证通过后根本不订阅任何内容 — 纯粹是自动化脚本在跑,可能是恶意刷注册量或测试系统漏洞。
第二类:真人薅免费版羊毛
免费版有 2 个订阅额度限制。有人利用 Gmail 别名([email protected]、[email protected])或多个不同邮箱反复注册新账号,每个账号白拿 2 个额度。不是机器人,是真人在操作,但一个人用了好几个账号。
当时的防护约等于零 — 只有一个邮箱唯一性检查,连 IP 限频都没做。
这些问题都是运营过程中遇到才去解决的。做 AI News RSS 时直接复用了之前项目积累的防护策略。同样的坑不需要踩第二次。
防护演进过程
不是一步到位设计出来的,是被打一次加一层:
第一次被攻击(临时邮箱批量注册)
→ 加临时邮箱域名黑名单
第二次(攻击者换了新的临时邮箱域名)
→ 加 IP 频率限制
第三次(用分布式 IP 绕过)
→ 加 Turnstile 人机验证
第四次(用 Gmail 的 +suffix 别名绕过邮箱唯一性)
→ 加邮箱归一化2
3
4
5
6
7
8
9
10
11
不要试图一开始就做完美的防护系统。 你不知道攻击者会用什么手段。先裸着上线,被打了看是什么攻击方式,再加对应的防护。性价比最高。
4 层防护详解
第 1 层:临时邮箱域名黑名单
源码 auth.py 里维护了一个 BLOCKED_EMAIL_DOMAINS 集合,包含几十个已知的临时邮箱域名:
- 主流临时邮箱服务(tempmail.com、guerrillamail.com、mailinator.com 等)
- 攻击中发现的新域名(tenvil.com、nuclene.com、vbbsc.store — 带注释标记发现时间和攻击者 IP)
- Firefox Relay(mozmail.com — 合法服务但被用来批量注册)
注册时检查邮箱的 @ 后面的域名,命中直接拒绝。
成本: 几乎为零。维护一个黑名单列表,发现新的加进去就行。
第 2 层:IP 频率限制
源码中的具体参数:
- 注册尝试: 同 IP 每小时最多 5 次(包括失败的)
- 注册成功: 同 IP 每 24 小时最多 1 次成功注册
用 Redis Sorted Set 实现滑动窗口计数。核心逻辑:删除过期记录 → 统计当前窗口内次数 → 超限则拒绝。
为什么还有内存兜底? 因为 Redis 可能挂。源码里 _check_ip_rate 用 Python dict 做备用存储,保证 Redis 不可用时限流仍然生效(虽然重启会丢数据,但总比完全没有限制好)。
第 3 层:Turnstile 人机验证
Cloudflare Turnstile — 免费、无感(用户不需要做验证码题目)、接入简单。
前端:注册表单加一个 Turnstile 组件,自动生成 token。 后端:拿 token 调 Cloudflare 的 siteverify API 验证。
用户注册 → 前端生成 Turnstile token → 后端验证 token → 通过才继续对普通用户完全无感,但能拦住大部分自动化脚本。
第 4 层:邮箱归一化
防止用邮箱别名绕过唯一性检查。源码中 _normalize_email 函数的处理:
- Gmail 的
.是无效的 —[email protected]和[email protected]是同一个邮箱 +suffix统一去掉 —[email protected]归一化为[email protected]- 覆盖主流邮箱:Gmail、Outlook、ProtonMail、QQ、163 等
注册时计算归一化邮箱存到 normalized_email 字段,同一归一化邮箱只能注册一次。
封禁策略
对确认恶意的账号:
- 标记
is_active = False - 记录
ban_reason(具体原因) - 登录时展示封禁原因 — 让误封的人知道为什么被封、怎么联系解封
还有临时冻结机制(banned_until)— 设一个解冻时间,到期自动恢复。适用于"疑似但不确定"的情况。
投入产出比
防薅这件事要控制投入,别过度工程:
| 措施 | 开发时间 | 效果 |
|---|---|---|
| 邮箱黑名单 | 10 分钟 | 拦住 50% 最低级攻击 |
| IP 频率限制 | 1 小时 | 拦住 80% 自动化注册 |
| Turnstile | 半天 | 拦住 95% 脚本攻击 |
| 邮箱归一化 | 1 小时 | 封堵别名绕过 |
| 手机号验证 | 费钱 + 复杂 | 效果好但对月收入百元的产品不值 |
对月收入百元级的产品来说,防护做到"攻击成本 > 收益"就够了。 100% 防不住所有人,但能把 99% 的低成本攻击拦住。