登录、注册、短信验证码接口是 CC 攻击最偏爱的目标:一次请求就要查库、校验密码哈希,开销远高于静态页;而且攻击者顺手就能撞库,一波流量干两件事。本文用 9 个问答讲清怎么区分正常登录高峰和撞库 CC、Nginx 层怎么单独限速登录接口、短信接口怎么防刷、以及账号维度风控规则该怎么设参数。
Q:为什么登录接口是 CC 攻击的头号目标?
三个原因叠在一起,让它成了性价比最高的靶子。
- 单次请求成本极高。静态页走缓存几乎不耗资源,登录接口每次都要查用户表、算一次密码哈希(bcrypt 这类算法本身设计得就慢)。几百 QPS 就能把 CPU 打满,攻击成本极低。
- 无法缓存、无法静态化。CDN 和页面缓存对 POST 登录请求完全无能为力,请求必然穿透到源站数据库。
- 顺手撞库,一次攻击两用。攻击者拿泄露的账号密码库批量试探,既有资源消耗效果,又可能撞开真实账号。所以很多登录 CC 的动机根本不是打挂你,而是刷账号。
还有一个容易被忽略的点:登录接口被攻击时,你的正常用户也登录不了,这是最直接的业务损失,比首页打不开严重得多。
Q:怎么区分正常登录高峰和撞库 CC?
靠五个维度交叉判断,不要只看请求量。
- 来源 IP 分散度:正常高峰集中在少数几个运营商网段;撞库通常是大量分散 IP(IP 池)或反过来——少数机房 IP 高并发。
- 账号名称的随机性:正常登录的账号名是真实用户名或手机号;撞库用的是泄露库里的账号,格式往往杂乱、含大量不存在的账号。
- 登录失败率:正常业务失败率通常在 10% 以内;撞库的失败率普遍 90% 以上,这是最强的单一信号。
- UA 与请求头缺失:脚本请求经常没有 Referer、Accept-Language,或者 UA 是 python-requests、Go-http-client 这类。
- 时间分布:正常高峰跟随业务作息(早 8 点到晚 10 点);撞库往往集中在凌晨。
先用日志确认,把登录请求单独捞出来看:
# 统计登录接口的来源 IP 排名
grep "POST /api/login" /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -30
# 统计登录请求的 UA 分布
grep "POST /api/login" /var/log/nginx/access.log | awk -F'"' '{print $6}' | sort | uniq -c | sort -rn | head -20
如果 UA 分布里出现 python-requests、curl 这类,或者某个 IP 在一分钟内打了上百次登录,基本可以直接定性。
Q:Nginx 层怎么给登录接口单独限速?
核心原则:登录接口必须用独立的限速 zone,绝不和全站共用一个。全站 zone 阈值放得宽,对登录接口等于没限;放得严,又会误伤正常浏览。
# 单独为登录接口建 zone:按真实客户端 IP 计时
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=loginconn:10m;
# 限流命中时返回 429 而不是默认 503
limit_req_status 429;
limit_conn_status 429;
location = /api/login {
limit_req zone=login burst=5 nodelay;
limit_conn loginconn 3; # 单 IP 最多 3 个并发登录
proxy_pass http://backend;
}
几个必须注意的点:
- rate=5r/m 不是拍脑袋定的。先算正常用户一天登录几次(一般不超过 20 次),再按「10 分钟内不超过 5 次」设阈值。这个量级对真人绝对宽松,对脚本极紧。
- burst 要留一点。用户输错密码重试、前端重复提交都可能短时连发,burst=5 给个缓冲,避免真人被误伤。
- 不要用 403 或 444。登录接口被封时,前端需要区分「被限流」和「密码错误」。返回 429 并带 JSON 提示,用户体验正常:
{"code":429,"msg":"操作过于频繁,请稍后再试"}。 - 注意 NAT 出口。公司、校园网、部分移动网络下成百上千用户共用一个出口 IP。如果站点用户集中在某类网络,阈值要相应放宽,或者改用账号维度而非 IP 维度来限制。
Q:只靠 Nginx 限速够吗?为什么必须联动业务风控?
不够,因为 Nginx 的视角只有 IP。
撞库攻击本来就用分布式 IP 池,单 IP 一分钟可能只请求一两次,永远碰不到 IP 阈值,但把 IP 数量乘以一千倍,总请求量足以打满数据库。这时候按 IP 限速完全失效。
而业务层能看到 Nginx 看不到的维度:
- 账号维度:同一个账号被不同 IP 尝试了多少次。正常用户换网络重试是有限的,几十个 IP 试同一个账号必然是撞库。
- 设备/会话维度:同一设备指纹、同一 Cookie 会话下的失败次数。
- 行为序列:先访问首页再点登录,还是直接 POST 登录接口?真人几乎都有前置行为,脚本一般是裸奔直连。
- 参数熵:账号字段的字符分布。真人账号名像用户名,撞库库里的账号名分布完全不同。
正确架构是三层叠加:边缘频控(挡掉 90% 粗糙流量)→ Nginx 独立限速(兜底止血)→ 业务风控(精准识别分布式撞库)。少了任何一层,都有绕过路径。
Q:业务层风控规则具体怎么设?给一组可落地的参数
下面这组规则可以直接照搬,数字按自身业务量微调:
- 账号维度:同账号连续失败 5 次(10 分钟内)→ 强制图形验证码;失败 10 次 → 账号临时锁定 15 分钟并告警;失败 20 次 → 锁定 1 小时 + 通知账号所有者。
- IP 维度:同 IP 1 小时内尝试过的不同账号数 > 20 个 → 直接拒绝并加入观察名单(注意不是看请求次数,是看账号数量,这是撞库的核心特征)。
- 设备维度:同设备指纹 24 小时内失败次数 > 30 → 触发验证码,> 100 → 拒绝。
- 来源维度:来源为机房 ASN 且无前置页面浏览行为 → 降低风控分,直接降级到验证码。
实现上不需要一上来就上商业风控系统,用 Redis 计数器就能落地:
# 账号维度失败计数(10 分钟窗口)
INCR login:fail:acct:{account}
EXPIRE login:fail:acct:{account} 600
# IP 维度尝试的账号集合(1 小时窗口)
SADD login:acct:ip:{client_ip} {account}
EXPIRE login:acct:ip:{client_ip} 3600
# 判定:SCARD login:acct:ip:{client_ip} > 20 即拒绝
这套逻辑很轻,但能精准命中撞库的关键特征——一个 IP 换很多账号、一个账号被很多 IP 试。这正是 Nginx 层永远看不到的东西。
Q:短信验证码接口被刷爆怎么办?
这是最烧钱的一类攻击,必须专门防。一条短信成本约 3 到 5 分钱,被刷 10 万条就是几千元真金白银,而且攻击方拿你的短信通道还能做轰炸。
三层限制缺一不可:
- 手机号维度:同一号码 60 秒 1 条、1 小时 5 条、1 天 10 条。
- IP 维度:同一 IP 1 小时最多 20 条、1 天 50 条。注意这里的阈值要远低于登录接口,因为短信是真金白银。
- 前置人机验证:发短信接口之前必须挂人机验证,绝不能裸奔。这一条能挡掉绝大多数脚本刷量。
# 短信接口独立的严格限速 zone(全站建议再叠一层)
limit_req_zone $binary_remote_addr zone=sms:10m rate=3r/m;
location = /api/sms/send {
limit_req zone=sms burst=2 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
另外两个细节:接口要做请求签名 + 时间戳 + 一次性令牌,防止被重放;短信发送量要单独做日限额告警,一旦当日发送量超过历史均值的三倍立即通知,很多时候攻击已经打了半小时你才发现。
Q:用了 CDN / WAF 之后,源站限速要注意什么?
要注意一个最容易踩的坑:源站拿到的客户端 IP 会变成 CDN 节点的 IP。
如果不做处理,Nginx 的 $binary_remote_addr 全是节点地址,限速键全撞在一个 key 上——结果是所有用户共享一个令牌桶,要么很快被打满导致全站 429,要么阈值被迫放到极宽而失效。所以必须正确还原真实客户端 IP:
# 信任 CDN 回源段,从 X-Forwarded-For 还原真实客户端 IP
set_real_ip_from 203.0.113.0/24; # 换成你的 CDN 节点回源段
set_real_ip_from 198.51.100.0/22;
real_ip_header X-Forwarded-For;
real_ip_recursive on; # 递归剥离,取最左侧非可信地址
# 回源时把真实 IP 传下去,供业务风控使用
proxy_set_header X-Real-IP $remote_addr;
验证方法很简单:在日志格式里加上 $remote_addr 和 $http_x_forwarded_for,如果两者永远是同一个地址且数量很少,说明真实 IP 没还原成功。
同时明确分层职责:边缘负责粗粒度频控与 IP 信誉,源站 Nginx 负责接口级限速兜底,业务风控负责账号维度精准识别。三层各管一段,任一层被绕过还有下一层接住。
Q:误伤真实用户怎么止损?
限速参数必然存在误伤概率,重点是让损失可控。
- 先观察再拦截。新规则先在日志模式跑 24 小时,统计会被命中的请求里有多少来自正常 UA、正常账号、正常城市。
- 阈值按 P99 设定。取正常用户行为分布的第 99 百分位作为阈值,误伤率天然控制在 1% 以内。
- 给自助解锁通道。被锁定的账号通过邮箱验证或人工客服可自助解锁,比统一等 15 分钟体验好得多。
- 白名单要显式维护。内部办公 IP、监控拨测 IP、支付与第三方回调 IP、压测 IP 一律加白,否则监控会先误报。注意登录接口通常不需要放行搜索引擎蜘蛛,别把蜘蛛白名单无脑套到所有接口上。
- 告警阈值要对准「异常」而不是「量」。429 数量本身不可怕,可怕的是失败率和不同账号数同时飙升,这才是撞库的特征。
Q:还有什么补充建议?
把处置流程固定成清单,出事时按顺序走,不要乱打:① 从日志确认是纯 CC 还是撞库(看失败率与账号分布)→ ② 登录与短信接口单独限速止血 → ③ 挂上人机验证做第一道门槛 → ④ 开启账号维度锁定与告警 → ⑤ 对确认的机房来源 IP 段/ASN 做封禁 → ⑥ 事后清理:通知异常账号、强制改密、核查是否有撞库成功的登录。
最后强调一条最容易被漏掉的:失败率 90% 不代表没有成功的 10%。撞库攻击者手里有真实账密库,一定有部分登录是成功的。所以处置完流量问题后,务必去翻登录成功日志,检查有没有来自陌生 IP、陌生设备、陌生城市(或异地同时登录)的成功记录,有就立即通知用户改密并踢下线。流量层面的 CC 你半小时能压住,账号被撞开的后果却是长期的。