登录接口被CC撞库怎么办?风控联动实战

登录、注册、短信验证码接口是 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 你半小时能压住,账号被撞开的后果却是长期的。