bruteforce.js
1.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* 防爆破 —— 窗口计数 + 阈值判定的纯逻辑(计数存储走注入的 repo,不用进程内 Map)。
*
* 判定逻辑(`evaluateAttempts`)是纯函数:给定窗口内已记录的失败次数、阈值、窗口长度,
* 返回是否锁定与 `retryAfterMs`。落库/读库由 repo 完成,本文件只决策。
*
* 默认阈值/窗口可由调用方覆写(开闭原则:改规则只动这里的常量或注入 opts)。
*/
export const DEFAULT_MAX_ATTEMPTS = 5
export const DEFAULT_WINDOW_MS = 15 * 60 * 1000 // 15 分钟
/**
* 把 email + ip 归一成防爆破计数 key(同标识合并计数)。
* @param {string} email
* @param {string} [ip]
* @returns {string}
*/
export function attemptKey(email, ip) {
const e = String(email ?? '').trim().toLowerCase()
return ip ? `${e}|${ip}` : e
}
/**
* 纯判定:当前窗口失败次数是否已达阈值;若是,给出还需等待多久。
*
* `retryAfterMs` = 窗口长度(保守估计:最早一条失败记录在窗口边界时需等待整个窗口;
* 若提供 `oldestAt` 与 `now` 则给出精确剩余时间)。
*
* @param {Object} args
* @param {number} args.recentCount 窗口内已记录的失败次数
* @param {number} [args.maxAttempts]
* @param {number} [args.windowMs]
* @param {number} [args.oldestAt] 窗口内最早一条失败记录的时间戳(epoch ms),可选精确化
* @param {number} [args.now]
* @returns {{ locked: boolean, retryAfterMs: number }}
*/
export function evaluateAttempts({
recentCount,
maxAttempts = DEFAULT_MAX_ATTEMPTS,
windowMs = DEFAULT_WINDOW_MS,
oldestAt,
now = Date.now(),
}) {
const locked = recentCount >= maxAttempts
if (!locked) return { locked: false, retryAfterMs: 0 }
let retryAfterMs = windowMs
if (typeof oldestAt === 'number') {
// 最早一条失败记录滚出窗口后即可重试。
retryAfterMs = Math.max(0, oldestAt + windowMs - now)
}
return { locked: true, retryAfterMs }
}