38.7 账号安全 (撞库 / 暴力破解 / 防盗号 / 验证码)
一句话
账号安全的目标不是“密码足够复杂”,而是让撞库与爆破的成本高于收益,同时不让防御手段本身变成新的攻击面——锁死账号是 DoS,短信验证码是话费炸弹,MFA 才是撞库真正的终结者。
三种攻击,三种节奏
| 攻击 | 攻击者手上有什么 | 打谁 | 节奏特征 | 靠什么防 |
|---|---|---|---|---|
| 暴力破解 | 没有凭据 | 一个账号 | 高频、集中、可被限流 | 锁定/退避 + 强密码 + 验证码 |
| 撞库 | 别处泄露的“账号-密码”对 | 大量账号 | 低频、每账号一次、IP 分散 | MFA + 设备指纹 + 泄露库比对 |
| 盗号(ATO) | 钓鱼、木马、会话劫持 | 高价值账号 | 不猜密码,直接拿会话 | 会话保护 + 行为检测 + 二次验证 |
关键在于:撞库每个账号只试一次。所有“按账号”的阈值(5 次锁定、10 次限流)对它完全无效——它赌的是“你在别处用了同一个密码”。
数字一:爆破一个账号要多久
36 字符集(小写 + 数字),期望尝试次数 = 密码空间 ÷ 2:
| 密码长度 | 空间 | 10 次/秒(在线) | 1000 次/秒(分布式) |
|---|---|---|---|
| 6 位 | 2.18e9 | 3.5 年 | 12.6 天 |
| 8 位 | 2.82e12 | 4472 年 | 44.7 年 |
| 10 位 | 3.66e15 | 580 万年 | 6 万年 |
结论:
- 8 位随机密码在线爆破不可行,但 6 位在分布式爆破下 13 天就破——短密码必须配合限流与验证码。
- 真正危险的不是“在线猜”,而是离线拖库:拿到哈希后可以按每秒十亿次的速度跑,长度和算法(bcrypt/argon2)才是防线。
- 因此“密码复杂度策略”主要防的是弱口令字典,不是算力;撞库绕过复杂度策略,因为它用的是真实存在的密码。
数字二:锁定策略是一种 DoS 武器
“连续 5 次失败锁定账号”看起来稳妥,但攻击者可以批量触发:
锁定 1,000 个账号 → 5,000 次错误尝试 → 100 请求/秒 → 50 秒
锁定 10,000 个账号 → 50,000 次错误尝试 → 8 分钟
8 分钟就能让一万个真实用户登不进来(而且他们会反复尝试、打爆客服)。更糟的是“永久锁定/必须人工解锁”——攻击者用 5 次请求换你一次工单。
| 策略 | 抗爆破 | 抗 DoS | 体验 | 结论 |
|---|---|---|---|---|
| 永久锁定 | 强 | 极差 | 差 | 不要用 |
| 固定 30 分钟锁定 | 强 | 差 | 差 | 慎用 |
| 指数退避(1s/2s/4s…上限 1h) | 强 | 好 | 可接受 | 推荐 |
| 验证码挑战(失败 N 次后) | 强 | 好 | 中 | 推荐 |
| 只监控不阻断 | 弱 | 最好 | 好 | 仅适合低风险场景 |
🔑 锁定的正确形态是渐进式:失败次数越多、退避越长,但不把账号锁死;同时按“账号 + IP + 设备”三维计数,避免攻击者用少量账号反向锁死大量用户。
数字三:撞库为什么难防
100 万条泄露凭据、密码复用成功率 1.5%(保守估计)→ 15,000 个账号被接管。
| 分摊窗口 | 每小时尝试 | 每小时成功 | 检测难度 |
|---|---|---|---|
| 7 天 | 5,952 | 89 | 基线约 20 次/小时新设备成功 → 4.5 倍,可发现 |
| 30 天 | 1,389 | 21 | 约 1.04 倍,淹没在噪声里 |
按维度看检出率:
| 限流维度 | 攻击者分布 | 检出率 |
|---|---|---|
| 按账号(5 次/分钟) | 每账号只试 1 次 | 0 |
| 按 IP(20 次/分钟) | 1 万个代理 IP,每 IP 每小时 0.6 次 | 0 |
| 按设备指纹 | 每次换 UA/指纹 | 低 |
| 全局“新设备登录成功率” | 需要基线 + 关联分析 | 中 |
| MFA | 成功率高但拿不到第二因子 | 高 |
MFA 覆盖率对结果的削减:
| MFA 覆盖率 | 剩余被接管账号 |
|---|---|
| 0% | 15,000 |
| 50% | 7,500 |
| 90% | 1,500 |
| 99% | 150 |
🔑 撞库没有“阈值解”,只有“提高单次成功率的上限”:MFA 把“知道密码”变成“知道密码 + 持有设备”,一次撞库的收益从 1.5% 掉到接近 0。
验证码:按风险触发,而不是每次都要
| 类型 | 安全性 | 体验 | 成本 | 短板 |
|---|---|---|---|---|
| 无感(行为/设备指纹) | 中 | 最好 | 低 | 可被养号绕过 |
| 滑块/点选 | 中 | 中 | 低 | 打码平台可破 |
| 图形字符 | 低 | 差 | 低 | OCR 与打码平台 |
| 短信验证码 | 中 | 差 | 每条几分钱 | SIM swap、拦截、轰炸 |
| 邮箱验证码 | 中 | 中 | 低 | 邮箱被盗则失效 |
| TOTP(App) | 高 | 中 | 0 | 需要用户安装 |
| Passkey/WebAuthn | 最高 | 好(可免密) | 0 | 设备生态与兼容性 |
四个必须算的数字:
- 位数:4 位验证码空间 1 万,限 5 次尝试,猜中概率
5e-4;6 位是5e-6——差 200 倍。位数不够时,限流必须更严。 - 轰炸成本:攻击者用脚本触发 10 万条短信,按 $0.04/条 = $4,000 的话费,账单算你的。必须按“手机号 + IP + 设备”三维限流,并加发送间隔与图形验证。
- 有效期:5 分钟足够完成一次输入;越长越容易被截获重放。验证码必须一次性,用过即废。
- 重放:验证码要和“这次会话/这个手机号/这个用途”绑定,不能一个码用于登录又用于改密。
什么时候该加验证码:登录失败 ≥3 次、新设备 + 异地、注册、找回密码、改绑手机号、提现。不该加:每次正常登录(赶走用户)、每次查看(无风险)。
防盗号:登录成功之后才是战场
拿到密码只是开始,攻击者随后会做四件事:
| 动作 | 攻击手法 | 防御 |
|---|---|---|
| 保持登录 | 会话劫持、token 窃取 | HttpOnly/Secure/SameSite、短 TTL、绑定设备指纹、改密后 token_version+1 全局失效 |
| 改密/换绑 | 拿到会话后直接改 | 敏感操作二次验证(旧密码/短信/TOTP)、改绑冷静期、通知原手机号 |
| 转移资产 | 提现、下单、发券 | 大额二次确认、限额、风控名单、延迟到账 |
| 找回账号 | 社工客服、伪造身份 | 客服流程与权限、录音、人工复核、不可仅凭“身份证照片” |
两个可量化的检测信号:
- 不可能旅行:10 分钟内出现在 8000 公里外 → 速度 48,000 km/h,是客机巡航速度的 53 倍。阈值一般取 900–1000 km/h,超过就要求二次验证。
- 新设备 + 首次成功:撞库最典型的特征,按“账号维度”统计“从未成功过的设备首次成功”,比按 IP 更稳。
分层防御:一张表
| 层 | 手段 | 挡住 | 挡不住 |
|---|---|---|---|
| 凭据 | 长度 + 弱口令库 + 泄露密码比对 | 弱密码、已泄露密码 | 唯一但被钓鱼的强密码 |
| 频率 | 指数退避、三维限流 | 爆破、脚本 | 慢速分布式撞库 |
| 挑战 | 风险触发验证码、MFA | 自动化、撞库 | 实时钓鱼 OTP、SIM swap |
| 会话 | 短 TTL、设备绑定、HttpOnly | 会话劫持、XSS 窃取 | 终端被完全控制 |
| 行为 | 不可能旅行、新设备、操作序列 | 盗号后的异常 | 攻击者耐心模仿 |
| 流程 | 客服复核、冷静期、通知 | 社工、内部滥用 | 内部人员串通 |
可运行的账号安全账本
package main
import (
"fmt"
"math"
)
func human(sec float64) string {
switch {
case sec < 60:
return fmt.Sprintf("%.0f 秒", sec)
case sec < 3600:
return fmt.Sprintf("%.0f 分钟", sec/60)
case sec < 86400:
return fmt.Sprintf("%.1f 小时", sec/3600)
case sec < 86400*365:
return fmt.Sprintf("%.1f 天", sec/86400)
default:
years := sec / 86400 / 365
if years >= 10000 {
return fmt.Sprintf("%.0f 万年", years/10000)
}
return fmt.Sprintf("%.1f 年", years)
}
}
func main() {
// ① 在线爆破:36 字符集(小写+数字),期望尝试 = 空间 / 2
fmt.Println("① 密码空间与爆破时间(36 字符集,期望尝试 = 空间/2)")
for _, n := range []int{6, 8, 10} {
space := math.Pow(36, float64(n))
for _, rate := range []float64{10, 1000} {
fmt.Printf(" %2d 位密码:空间 %.2e,%4.0f 次/秒 → %s\n",
n, space, rate, human(space/2/rate))
}
}
// ② 锁定策略被当成 DoS 武器
const attemptsPerAccount = 5
for _, accounts := range []int{1000, 10000} {
req := float64(accounts * attemptsPerAccount)
fmt.Printf("② 锁定 %d 个账号:需要 %.0f 次错误尝试,100 请求/秒 → %s\n",
accounts, req, human(req/100))
}
// ③ 撞库:慢速、每账号只试一次,绕开所有“按账号”的阈值
leaked, reuseRate := 1_000_000.0, 0.015
days := 7.0
success := leaked * reuseRate
perHour := leaked / days / 24
fmt.Printf("③ 撞库:%.0f 万条凭据、复用成功率 %.1f%% → %.0f 个账号被接管\n",
leaked/10000, reuseRate*100, success)
fmt.Printf(" 分摊到 %.0f 天:每小时 %.0f 次尝试、%.0f 次成功;每个账号只试 1 次 → 按账号限流检出 0\n",
days, perHour, perHour*reuseRate)
fmt.Printf(" 1 万个代理 IP 分摊:每 IP 每小时 %.2f 次 → 按 IP 限流也检出 0\n", perHour/10000)
fmt.Printf(" 分摊到 7 天:每小时 %.0f 次新设备成功 vs 基线 20 次(%.1f 倍)→ 全局异常可检出\n",
perHour*reuseRate, perHour*reuseRate/20)
slow := leaked / 30 / 24
fmt.Printf(" 分摊到 30 天:每小时 %.0f 次成功 vs 基线 20 次(%.2f 倍)→ 淹没在噪声里,只能靠“失败率/设备指纹”关联\n",
slow*reuseRate, slow*reuseRate/20)
// ④ 验证码:位数决定穷举难度
for _, digits := range []int{4, 6} {
space := math.Pow(10, float64(digits))
fmt.Printf("④ %d 位验证码:空间 %.0f,限 5 次尝试 → 猜中概率 %.1e\n",
digits, space, 5/space)
}
const smsCost = 0.04
fmt.Printf(" 短信轰炸:10 万条 × $%.2f = $%.0f;按手机号 + IP + 设备三维限流才能压住\n",
smsCost, 100000*smsCost)
// ⑤ 不可能旅行
distKm, minutes := 8000.0, 10.0
speed := distKm / (minutes / 60)
fmt.Printf("⑤ 不可能旅行:%.0f km / %.0f 分钟 = %.0f km/h,是客机巡航速度的 %.0f 倍\n",
distKm, minutes, speed, speed/900)
// ⑥ MFA 覆盖率对撞库成功率的削减
for _, coverage := range []float64{0.5, 0.9, 0.99} {
fmt.Printf("⑥ MFA 覆盖率 %.0f%% → 撞库成功从 %.0f 降到 %.0f\n",
coverage*100, success, success*(1-coverage))
}
}
输出:
① 密码空间与爆破时间(36 字符集,期望尝试 = 空间/2)
6 位密码:空间 2.18e+09, 10 次/秒 → 3.5 年
6 位密码:空间 2.18e+09,1000 次/秒 → 12.6 天
8 位密码:空间 2.82e+12, 10 次/秒 → 4472.8 年
8 位密码:空间 2.82e+12,1000 次/秒 → 44.7 年
10 位密码:空间 3.66e+15, 10 次/秒 → 580 万年
10 位密码:空间 3.66e+15,1000 次/秒 → 6 万年
② 锁定 1000 个账号:需要 5000 次错误尝试,100 请求/秒 → 50 秒
② 锁定 10000 个账号:需要 50000 次错误尝试,100 请求/秒 → 8 分钟
③ 撞库:100 万条凭据、复用成功率 1.5% → 15000 个账号被接管
分摊到 7 天:每小时 5952 次尝试、89 次成功;每个账号只试 1 次 → 按账号限流检出 0
1 万个代理 IP 分摊:每 IP 每小时 0.60 次 → 按 IP 限流也检出 0
分摊到 7 天:每小时 89 次新设备成功 vs 基线 20 次(4.5 倍)→ 全局异常可检出
分摊到 30 天:每小时 21 次成功 vs 基线 20 次(1.04 倍)→ 淹没在噪声里,只能靠“失败率/设备指纹”关联
④ 4 位验证码:空间 10000,限 5 次尝试 → 猜中概率 5.0e-04
④ 6 位验证码:空间 1000000,限 5 次尝试 → 猜中概率 5.0e-06
短信轰炸:10 万条 × $0.04 = $4000;按手机号 + IP + 设备三维限流才能压住
⑤ 不可能旅行:8000 km / 10 分钟 = 48000 km/h,是客机巡航速度的 53 倍
⑥ MFA 覆盖率 50% → 撞库成功从 15000 降到 7500
⑥ MFA 覆盖率 90% → 撞库成功从 15000 降到 1500
⑥ MFA 覆盖率 99% → 撞库成功从 15000 降到 150
参数怎么给
| 场景 | 策略 |
|---|---|
| 普通登录 | 指数退避(失败 3 次后 1s、2s、4s…上限 1h)+ 新设备二次验证 |
| 注册 | 图形/滑块 + 手机号/IP 限流 + 一次性验证码 |
| 找回密码 | 邮箱/短信二选一 + 风控评分;不要只凭“安全问题” |
| 改密/换绑/提现 | MFA 二次验证 + 冷静期 + 原手机号通知 |
| 高价值账号(管理员/资金) | Passkey/硬件 key,禁止仅密码登录 |
| 内部客服后台 | 最小权限 + 全程审计 + 双人复核 |
反模式
| 反模式 | 后果 |
|---|---|
| 永久锁定账号 | 攻击者 5 次请求锁死一个用户,客服被打爆 |
| 只按账号限流 | 撞库每账号一次,检出率为 0 |
| 只按 IP 限流 | 代理池分摊后每 IP 每小时不到 1 次 |
| 验证码加在每次登录 | 用户流失,且打码平台照样破 |
| 4 位短信码不严限流 | 猜中概率 5e-4,10 万次尝试就能中 50 次 |
| 验证码可重复使用 | 截获后可重放 |
| 改密后不失效旧会话 | 攻击者保持登录 |
| 只凭身份证照片找回账号 | 社工与证件伪造直接接管 |
| 只防登录、不防操作 | 登录正常,提现/改绑被滥用 |
| 密码复杂度当唯一防线 | 撞库用的就是“符合复杂度”的真实密码 |
自检
Q1 每账号“连续 5 次失败锁定 30 分钟”,为什么对撞库完全无效?该改什么?
▸ 答案:撞库对每个账号只尝试 1 次(用别处泄露的真实密码),永远触发不了“连续 5 次失败”的阈值;把 100 万条尝试分摊到 7 天、1 万个代理 IP 后,每 IP 每小时不到 1 次,按 IP 的限流也看不见。真正有效的是提高单次尝试的成本:MFA(把成功率从 1.5% 压到接近 0)、设备指纹与可信设备、泄露密码库比对(命中即强制改密/二次验证),以及“新设备首次登录成功率”这类全局统计异常检测。
Q2 为什么“永久锁定 / 必须人工解锁”是危险设计?用数字说明。
▸ 答案:锁定是攻击者可以免费触发的副作用。5 次错误尝试就能锁死一个账号,1 万个账号只需要 5 万次请求——在 100 请求/秒下是 8 分钟。攻击者用极低成本制造“大面积无法登录 + 客服工单洪峰”,本质是用你的防御机制打你。正确做法是渐进退避(1s/2s/4s…上限 1h)、按三维计数、以及失败多次后改用验证码挑战,而不是把账号锁死。
Q3 6 位短信验证码安全吗?它的真正短板是什么?
▸ 答案:6 位空间 100 万,限 5 次尝试时猜中概率 5e-6,暴力猜解不是短板。短板在分发渠道:SIM swap(补卡)、短信拦截/木马、以及短信轰炸(10 万条 × $0.04 = $4,000 的话费)。另外运营商短信可能延迟,用户会重复点击“发送”,进一步放大成本。所以短信验证码应作为中等级别因子,高价值操作改用 TOTP 或 Passkey;并对发送做手机号 + IP + 设备三维限流、一次性使用、5 分钟过期。
Q4 MFA 覆盖率 90% 时,撞库的 15,000 个成功账号会降到多少?为什么不是 0?
▸ 答案:降到约 1,500(15,000 × 10%)。不是 0 的原因有三:① 剩下 10% 没开 MFA 的用户仍然可被接管;② 开了 MFA 的用户也可能被实时钓鱼(把 OTP 骗到手)或 SIM swap;③ “MFA 疲劳轰炸”(反复推送直到用户点同意)在推送式 MFA 上有效。所以还要补:号码匹配(number matching)、防疲劳限速、以及 Passkey 这类抗钓鱼因子。
Q5 “不可能旅行”的阈值怎么定?定得太严/太松分别有什么后果?
▸ 答案:阈值取“商用客机巡航速度”量级,约 900–1000 km/h;两地距离 ÷ 时间间隔 超过阈值就判定异常(例:10 分钟 8000 公里 = 48,000 km/h,是阈值的 53 倍)。定太严会误伤:VPN 出口跳变、运营商 NAT、用户真的坐飞机(登机前登录一次、落地后登录一次,时间差覆盖飞行时间则正常);定太松则形同虚设。工程上通常配合“可信设备/常用地点”降级处理:异常但设备可信 → 只要求二次验证,不直接封号。
跨篇相关:认证协议(Session / JWT / OAuth2 / OIDC)见 38.2 认证 (Session / JWT / OAuth2 / OIDC);授权与权限模型见 38.3 授权 (RBAC / ABAC / 权限模型);验证码与登录接口的注入/CSRF 风险见 38.6 常见攻击;登录态与令牌生命周期见 49.5 登录态与令牌生命周期;风控侧的规则与名单见 68.4 名单与画像(待补)。