接口限流与防爬:登录、图书、评论和上传场景怎么做
本文目录
限流的目标是保护可用性、降低自动滥用和给用户公平的资源预算;它不能证明请求者是人,也不能阻止已获授权读者截图或保存可见内容。书籍内容若必须保密,首先要有登录、逐资源授权和私有存储,再叠加限速、审计和短期 URL。不要宣称任何网页方案能绝对防复制。
按风险分层
- 登录/邮箱验证码:按账号标识、来源 IP、设备/会话与全局容量组合计数。账号维度保护账户,IP 维度限制单来源,但企业 NAT/校园网络会共用 IP,不能只靠 IP 锁死正常用户。
- 搜索/文章接口:按用户/IP 限制高成本查询,限制页大小、最大深度、筛选字段和并发;爬取公开文章无法靠验证码长期解决,应以缓存、分页和合理 robots/服务容量策略应对。
- 评论/反馈:要求登录或验证会话,按账号与目标文章限制频率,去重重复提交,加入审核与举报;不要用单一词库静默吞掉所有内容。
- 书籍文件:认证后逐本授权;文件读取带短过期签名、范围请求和并发限制,记录访问频率和账号维度告警。签名 URL 是 bearer credential,应短效并避免放入日志/Referer。
- 上传:Nginx、应用层同时限制体积、频率、同时处理数和总配额;校验扩展名、MIME、文件签名、解析时长和存储容量。上传限制与反爬不是同一件事。
选择算法和计数维度
固定窗口容易在窗口边界被突发两次;滑动窗口更平滑但需要更多状态;令牌桶允许受控突发并限制长期平均速率。根据端点成本配置不同预算:发验证码、搜索、下载和图片转换不应共用一个阈值。给每个身份设置并发上限,避免大量慢请求占住 worker。
在多实例下,进程本地 Map 会让每个节点各自放行完整额度,也会在重启时清零。共享 Redis/网关计数要使用原子操作、TTL 和明确的键空间。限流服务不可用时按端点风险选择 fail-open 或 fail-closed:登录保护/大文件处理常需保护资源,简单公开读取则可用缓存和全局熔断降级。这个选择应写入威胁模型。
端点预算:先限制成本,再识别身份
一条全局阈值无法同时保护账号、数据库、CPU、邮箱供应商和文件带宽。先给每个路由标注身份要求、资源成本、可接受突发量、目标延迟、单用户预算和全局容量;匿名用户先过网关级的粗粒度预算,完成认证后再按账号、租户和资源加细粒度预算。限制分页大小、批处理条数、上传字节数、解压后体积、并发转换数和单个请求的执行时间,避免低频的大请求绕过 QPS 限流。
| 场景 | 计数维度 | 重点限制 |
|---|---|---|
| 密码/邮箱登录 | 账号摘要、来源网络、全局 | 猜测尝试、发送频率、并行验证 |
| 搜索与列表 | 用户或可信来源、查询成本 | 页大小、查询深度、复杂筛选、并发 |
| 图书阅读 | 登录用户、具体图书、全局带宽 | 元数据访问、内容分段、范围请求、刷新频率 |
| 评论与投票 | 用户、文章、操作类型 | 重复提交、短时突发、未审核队列规模 |
| 上传与转换 | 用户配额、文件、转换队列 | 字节数、像素/解压尺寸、并发与最长处理时间 |
阈值来自真实资源预算和误拒成本,而不是某个教程里的固定数字。短窗口控制突发,长窗口控制持续成本;若用滑动窗口或令牌桶,应把同一策略的算法、边界和计数单位写清楚,避免前端、网关和应用各算一套互相矛盾的配额。
邮箱验证码和登录
发送验证码接口本身就是昂贵的外部副作用。按规范化邮箱、可信来源 IP、收件地址摘要、单地址冷却时间和全局邮件预算分别限流;IP 必须由受信代理链解析,不能直接相信公网请求的 X-Forwarded-For。对存在与不存在的邮箱尽量返回相同外部响应,并把发送状态作为受控的服务端状态处理。Redis 键、监控标签和普通日志不应暴露验证码;邮箱地址也应使用带密钥摘要作为键后缀,避免低熵地址被离线枚举。
发送流程:预占、投递、条件提交
先验证 SMTP 主机、凭据、发件地址和端口,再规范化邮箱并计算 IP/地址限额。用一段 Redis Lua 脚本原子地检查地址小时上限、占用短冷却锁并记录本次随机请求 ID;锁已存在时不应重复发信。验证码使用密码学安全随机源生成,邮件发出后才把验证码校验材料写入 Redis,并通过第二段 Lua 脚本确认冷却锁仍属于本次请求。这样较慢的旧 SMTP 请求不能覆盖更新的验证码。
const { createHmac, timingSafeEqual } = require('node:crypto');
function digestReaderCode(secret, rawEmail, purpose, code) {
const normalizedEmail = rawEmail.trim().toLowerCase();
return createHmac('sha256', secret)
.update(['email-otp:v2', purpose, normalizedEmail, code].join('\0'))
.digest('hex');
}
function matchesReaderCode(savedHex, secret, rawEmail, purpose, code) {
const saved = Buffer.from(savedHex, 'hex');
const candidate = Buffer.from(digestReaderCode(secret, rawEmail, purpose, code), 'hex');
return saved.length === candidate.length && timingSafeEqual(saved, candidate);
}
把六位码的明文只交给邮件传输过程,不写入 Redis、业务日志、错误响应或分析事件。普通 SHA-256 仍允许拿到 Redis 数据的人离线枚举一百万种六位组合;用服务端独立密钥或受保护的高熵密钥做 HMAC,并在消息中绑定版本、用途与规范化目标邮箱。HMAC 不能阻止在线猜码,因此仍要限制发送频率、校验尝试次数和登录请求速率。密钥不可发给浏览器,也不要为了“可恢复验证码”而加密后长期保存明文。
消费流程:验证与一次性失效
取出 Redis 里的摘要后,先递增该地址的错误尝试计数;超过上限时让码与计数一起失效。对用户输入按同一上下文计算候选摘要,固定长度校验后使用 timingSafeEqual 比较,不能用普通字符串相等来比较秘密。比较成功后仍需 Redis Lua 原子地再次确认摘要未变,并同时删除验证码和错误计数。否则两个并发登录请求可能都先读到同一有效码,再各自创建会话。
验证码要短时有效、单次使用、有限次猜测,并与用途和目标账号绑定。当前博客把邮箱登录与带密码注册作为同一类读者身份验证;新增改密、邮箱变更或支付等用途时,必须使用单独用途上下文、独立键空间和相应的二次确认,不能复用读者登录码。发送界面上的 60 秒倒计时只是体验提示,服务端 Redis 冷却和小时预算才是权威限制。
邮件服务失败边界
SMTP 服务器接受邮件与 Redis 提交验证码不在同一个事务里:邮件可能已经离开应用,但 Redis 随后不可用,用户会收到暂时无法验证的旧邮件。请求 ID 可以避免并发覆盖,失败时也只能释放自己持有的冷却锁、保留此前有效码;它无法让两个系统实现 exactly-once。需要更强投递可靠性时,考虑数据库 outbox/持久任务、稳定事件 ID、有限重试、死信告警和提供商回执,而不是在 HTTP 请求里无限重试。向用户返回“稍后重试”时,也要提示晚到邮件中的验证码可能已过期。
用 Resend 等服务时,SMTP/API 密钥只放在服务器密钥配置,发件域必须先完成供应商要求的验证。设置连接、握手和套接字超时;供应商临时不可用时做有限退避和告警,不记录完整邮件正文。账号或邮箱不存在、Redis 故障、发件域无效、服务商拒绝、响应超时和“邮件已接受但验证码提交失败”都需要单独的监控分类,但外部错误文案不能泄露账户是否存在或验证码内容。
测试至少覆盖:未知邮箱与已注册邮箱的响应一致;并发发送只允许一个请求持有冷却锁;较慢的旧发送不能覆盖新码;SMTP 失败时只释放自己的锁且不删除旧码;错误尝试达到阈值后验证码被原子清理;同一码并发消费最多成功一次;服务重启、Redis 超时、邮件晚到和验证码过期都有明确结果。日志、审计和指标不得携带验证码、完整邮箱、认证头或 SMTP 密钥。
参考:OWASP Authentication Cheat Sheet、Node.js HMAC API、Node.js timingSafeEqual、Redis Lua scripting 与 Nodemailer SMTP transport。密码登录还要防止攻击者反复触发永久账号锁定造成拒绝服务;短期延迟、风险挑战和恢复通道通常比永久锁号更可控。
图书访问:登录不是授权,签名不是权限
先确认账户有效,再检查当前图书是否已发布/上架、用户是否仍有读取权限、文件是否在私有存储中。不能只在页面上隐藏按钮,也不能在上传完成时给公开永久 URL。阅读内容接口应按图书和用户再次授权;签名凭证只用于短期传输,不能替代授权数据库里的当前状态。
async function issueBookReadToken({ principal, bookId }, deps) {
if (!principal) return { status: 401 };
const book = await deps.books.findReadable(bookId);
if (!book || !book.isPublished) return { status: 404 };
const allowed = await deps.authorization.canReadBook({
userId: principal.userId,
bookId: book.id,
});
if (!allowed) return { status: 404 };
const budget = await deps.limiter.consume({
key: ['book-read', principal.userId, book.id].join(':'),
policy: 'book-read',
});
if (!budget.allowed) {
return {
status: 429,
headers: { 'Retry-After': String(budget.retryAfterSeconds) },
};
}
const token = await deps.signer.issue({
subject: principal.userId,
bookId: book.id,
expiresInSeconds: 120,
});
return { status: 200, token };
}
这是表达顺序的伪依赖接口,签名格式与有效期应由服务端统一实现。真实系统应先在网关做低成本来源限流,再执行数据库授权与用户维度配额;正文分段/Range 请求需要有独立带宽与并发预算。短期 URL 是 bearer credential:避免放进 Referer、分析事件、错误日志和可公开缓存;使用私有存储、短有效期和按需重新授权。已获授权的读者仍可能截屏或保存内容,技术上无法保证绝对防复制。
评论、上传和转换任务
评论接口按作者/文章做重复提交去重和速率限制,同时保留审核、申诉、删除与举报路径。未审核内容要有队列容量上限,不能让脚本通过大量新账号把审核队列拖垮。投票要以服务端身份或有期限的防重复凭证计数,不能直接相信客户端上报的总票数。
上传需要网关和应用层共同设限:请求字节数、文件数量、用户配额、并行上传、转换队列深度和 worker 处理时长都要有上界。先验证上传错误、扩展名白名单、文件签名、解析器和解压/图像尺寸,再把原文件放到不可执行的私有存储。压缩包要限制条目数、展开后字节数、路径穿越和递归深度;限流不能代替恶意文件扫描和格式校验。
限流器的分布式语义
多实例服务不能用进程内 Map 作为最终预算:它会按实例数放大额度,也会在进程重启时丢失状态。Redis 或网关计数必须让“检查并扣减”成为原子操作,并设置过期清理、命名空间版本和键长度上限。不同接口使用稳定但不包含秘密的摘要键;不要把 token、验证码或原始 IP 放进指标标签。
限流存储短暂不可用时,是否放行取决于业务成本:对公开缓存读取可启用有上限的降级,对邮件发送、登录猜测、昂贵转换应暂停或使用更保守的本机熔断额度。fail-open 不能成为无限制放行;fail-closed 也要避免锁死管理员或健康恢复所需的操作。每种策略都要有到期、告警和人工恢复流程。
可信 IP 和反向代理
只有来自明确受信的反向代理才能提供客户端 IP header。公网请求可伪造 X-Forwarded-For;如果框架信任任意跳数或所有来源,攻击者可换 header 绕过 IP 配额。校验代理地址列表、跳数与真实 socket 地址,配置变更后做伪造 header 回归测试。不要把用户可控的 UA、Referer 或 IP 当认证因素。
响应和恢复
限流响应应使用 HTTP 429,并在可计算时提供 Retry-After 或剩余配额信息。错误体避免泄露内部阈值、账号是否存在或其他用户的限制状态。UI 清晰显示等待时间,自动重试采用抖动并遵守 Retry-After;客户端重试不能制造新的一轮请求风暴。
观察拒绝数、正常用户误拒比例、热点 IP/账号、每端点成本、文件带宽和规则命中。临时封禁要有到期时间、申诉/人工解除和攻击期间安全策略。禁止把完整请求体、验证码、书籍 token 或签名 URL 写进审计日志。
测试场景
- 同一 IP 多账号、同一账号多 IP、NAT 下多个合法用户并发,确认规则既保护账户也不轻易封整网。
- 通过伪造 X-Forwarded-For 重放,验证只有可信代理 header 生效。
- 在窗口边界发突发请求、Redis 重启、限流实例重试和时钟偏差下验证预算不会异常放大。
- 用匿名、登录、被封禁、书籍下架、过期签名访问内容/下载接口。
- 超大、伪装 MIME、损坏压缩包、慢上传和并发解析均受到资源上限约束。
- 检查 429、Retry-After、客户端退避、指标、告警与解除封禁流程。
参考:OWASP Authentication Cheat Sheet 的登录限速建议、OWASP REST Security Cheat Sheet 的 429/413 语义、OWASP File Upload Cheat Sheet 的上传防护,以及 OWASP API4:2023 的资源消耗风险。阈值应从正常流量、服务器预算和误拒成本测量,不能从示例数字照搬。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。