账号攻击正在从“同一个 IP 连续尝试密码”变成分布式、低频率、会主动调整行为的自动化流程。攻击者可以借助 AI 生成更像真人的请求,轮换 IP、设备指纹和请求节奏,从而绕过只检查单次请求的无状态安全规则。
Cloudflare 的 Account Abuse Protection dashboard 提供了另一种调查视角:通过有状态分析,把多个看似无关的请求串成一条行为链,并使用边缘生成的 Hashed User ID(哈希用户标识)追踪针对同一账号的活动。重点不再只是“这个请求可不可疑”,而是“这个账号在一段时间内经历了什么”。
为什么逐请求判断越来越不够用
传统规则通常观察单个请求或很短的时间窗口,例如:
- 某个 IP 在一分钟内提交了多少次登录请求;
- User-Agent 是否异常;
- 请求是否来自已知代理网络;
- 单次请求的 Bot 分数是否低于阈值。
这些信号仍然有价值,但攻击者可以把一次集中攻击拆散:使用数百个出口 IP,每个 IP 只请求一两次;模拟正常浏览器头部;在失败后改变账号、密码字典和等待时间。单独查看每个请求时,它们可能都没有越过封禁阈值。
有状态分析改变了聚合维度。安全团队可以围绕账号而不是 IP 建立时间线,回答更接近事件本质的问题:
- 同一账号是否在短时间内被大量不同网络尝试登录?
- 登录失败之后,是否紧接着出现密码重置、MFA 挑战或令牌刷新?
- 多个看似正常的请求,是否共同指向同一个用户标识?
- 攻击是否在更换 IP 后继续沿用相同的业务目标?
IP、设备和会话都可能变化,账号目标往往更稳定。Hashed User ID 就是用来连接这些离散事件的“线”。
Hashed User ID 的价值与边界
边缘生成的哈希用户标识可以让安全系统在不直接使用明文邮箱、用户名等信息的情况下,对同一用户的请求进行关联。调查人员随后可以按该标识查看登录、注册、密码重置或其他敏感流程中的连续行为。
这里需要区分“哈希化”和“匿名化”。哈希标识通常仍属于可关联的假名数据,并不天然等于匿名数据。实施时应注意:
- 不要直接对邮箱做普通 SHA-256。邮箱空间可枚举,攻击者可以预先计算常见地址的哈希;
- 更适合使用带服务端密钥的 HMAC,让外部人员无法离线建立明文映射表;
- 优先选择稳定的内部账号 ID,而不是可能改变、存在别名或大小写问题的邮箱;
- 多租户系统应把租户 ID 纳入输入,避免不同租户中的相同用户名碰撞;
- 不应把完整哈希返回给浏览器,也不能信任客户端自己提交的用户标识;
- 密钥轮换会改变标识,需要预先设计调查窗口和迁移策略。
Hashed User ID 适合做关联键,但不能单独作为封禁依据。共享账号、企业出口网络、密码管理器重试以及用户误操作,都可能形成类似自动化攻击的行为。
可以这样实践:在边缘生成稳定的 HMAC 标识
下面是一个可改造的 Cloudflare Worker 示例。它不是 Account Abuse Protection dashboard 的官方配置,也不代表产品内部的具体算法;它演示的是同一种工程模式:在边缘读取登录标识、生成带密钥的 HMAC,并覆盖转发给源站的内部请求头。
运行前需要把 app.example.com 改为不会再次命中同一 Worker 路由的源站域名,并根据实际登录 JSON 字段调整 email。
export default {
async fetch(request, env) {
const incomingUrl = new URL(request.url);
const headers = new Headers(request.headers);
// 永远不信任客户端传入的关联标识。
headers.delete('X-Hashed-User-ID');
if (request.method === 'POST' && incomingUrl.pathname === '/login') {
let payload;
try {
payload = await request.clone().json();
} catch {
return new Response('Invalid JSON', { status: 400 });
}
const loginName = String(payload.email ?? '')
.trim()
.toLowerCase();
if (loginName) {
const hashedUserId = await hmacHex(env.HUID_SECRET, loginName);
headers.set('X-Hashed-User-ID', hashedUserId);
}
}
const upstreamUrl = new URL(request.url);
upstreamUrl.protocol = 'https:';
upstreamUrl.hostname = env.ORIGIN_HOST;
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
const upstreamRequest = new Request(upstreamUrl.toString(), {
method: request.method,
headers,
body: hasBody ? request.body : undefined,
redirect: 'manual'
});
return fetch(upstreamRequest);
}
};
async function hmacHex(secret, value) {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign(
'HMAC',
key,
encoder.encode(value)
);
return [...new Uint8Array(signature)]
.map(byte => byte.toString(16).padStart(2, '0'))
.join('');
}
可以用下面的最小配置部署。将日期更新为部署时使用的 Workers compatibility date:
name = 'account-abuse-edge-id'
main = 'src/index.js'
compatibility_date = '2025-01-01'
[vars]
ORIGIN_HOST = 'app.example.com'
密钥不要写入代码或普通变量,使用 secret 管理:
npx wrangler secret put HUID_SECRET
npx wrangler deploy
生产环境还应补充三个约束:源站只接受来自可信边缘的该请求头;日志系统限制对标识的访问和保留时间;在用户成功认证后,尽快改用稳定的内部账号 ID,而不是长期依赖邮箱规范化结果。
从 dashboard 中沿线调查,而不是看到峰值就封禁
面对登录失败峰值,可以采用下面的调查顺序:
- 按 Hashed User ID 聚合事件,确认攻击集中在少量账号,还是横向扫描大量账号。
- 展开目标账号的时间线,比较登录失败、成功登录、MFA、密码重置和会话刷新之间的顺序。
- 再叠加 IP、ASN、地理位置、设备和请求特征,判断攻击者是否在主动轮换基础设施。
- 检查成功结果。大量失败值得关注,但“失败之后突然成功”通常更需要立即响应。
- 根据置信度分层处置:限速、交互式挑战、临时冻结敏感操作、撤销会话,或要求重新验证身份。
不要把一个 HUID 的异常直接扩展成整个 IP 段的永久封禁。企业 NAT、移动网络和代理服务会让大量正常用户共享网络特征。更稳妥的策略是把账号维度、网络维度、行为序列和认证结果组合起来。
上线前的检查清单
- 明确需要保护的流程:登录、注册、找回密码、优惠领取还是支付操作;
- 为同一业务主体选择稳定且规范化的标识;
- 使用 HMAC 或平台提供的边缘哈希能力,不记录不必要的明文身份数据;
- 丢弃客户端伪造的关联头,只允许边缘重新生成;
- 先观察基线和误报,再启用自动阻断;
- 为客服、安全和隐私团队建立 HUID 到内部事件的受控调查流程;
- 给密钥轮换、数据保留和跨区域合规留出设计空间。
有状态分析不是对 WAF、Bot 管理或速率限制的替代,而是补上它们缺少的时间与身份上下文。真正有效的账号保护,应当既能拦住当前请求,也能沿着同一账号留下的线索,看清分布式攻击的完整过程。