pub struct Throttle<S: ThrottleStore> { /* private fields */ }Expand description
滑动窗口限流 / 封禁闸门。
§key 由调用方构造
key 是计数桶的名字,本模块只把它当作不透明字符串。约定形如
format!("ip:{}", ip)(按来源)或 format!("acct:{}", user)(按账户);
两类 key 前缀不同,天然互不干扰,可同时启用。
不要把用户输入直接当 key:拿请求里原样的用户名 / 任意 header 当 key, 攻击者只要每次换一个值就能把自己拆成无限多个桶,限流形同虚设; 空 key 同理,会让所有构造失败的请求共用同一个桶。调用方须先规范化 (截断长度、统一大小写、限制字符集),并保证 key 非空。
Implementations§
Source§impl<S: ThrottleStore> Throttle<S>
impl<S: ThrottleStore> Throttle<S>
pub fn new(store: S, config: ThrottleConfig) -> Self
pub fn config(&self) -> &ThrottleConfig
Sourcepub fn check(&self, key: &str, now: u64) -> ThrottleDecision
pub fn check(&self, key: &str, now: u64) -> ThrottleDecision
请求进入时调用。先查封禁,再算剩余额度。
存储报错 → ThrottleDecision::Unavailable(见该变体的说明,不 fail-closed):
这里是限流的纵深防御,不是主认证闸门。后端故障时返回 Banned 会把全体
用户挡在门外(自我 DoS,且攻击者可能主动诱发),而放行只是暂时失去
暴力破解防护 —— 主认证闸门 SessionGuard 仍然在拦。调用方拿到
Unavailable 后自行选择,建议放行 + 告警。
Sourcepub fn check_any(&self, keys: &[&str], now: u64) -> ThrottleDecision
pub fn check_any(&self, keys: &[&str], now: u64) -> ThrottleDecision
同时检查多个维度(如 [ip_key, account_key]),返回最严格的结果。
合并规则(严格度):任一 Banned → Banned(取最晚的 until);
否则任一 Unavailable → Unavailable;否则 Allow 取最小 remaining。
每个 key 各自独立查询,不合并计数:ip: 与 acct: 是两类互不干扰的桶,
合并会让 NAT 后面的其他人替攻击者吃掉额度。
空 keys 什么都查不到,返回 Allow { remaining: 0 } 而非满额 —— 这个数字
会被写进 X-RateLimit-* 响应头,凭空报满额等于谎报额度。
Sourcepub fn record_failure(
&self,
key: &str,
now: u64,
) -> Result<ThrottleOutcome, StoreError>
pub fn record_failure( &self, key: &str, now: u64, ) -> Result<ThrottleOutcome, StoreError>
认证失败时调用。达到 threshold 就封禁并返回 Banned,否则返回 Allow { remaining }。
返回 ThrottleOutcome 而非 ThrottleDecision:这里不存在 Unavailable
—— 存储故障走 Err,两个可达状态对应两个分支,调用方不必再写一个永不执行的
第三个臂。判断「要不要拦这个请求」用 Throttle::check_any。
判定顺序:先拿窗口内计数,count >= threshold 时写封禁并返回解封时刻。
threshold 是「第几次失败触发封禁」,因此第 threshold 次调用返回的是
Banned 而不是 Allow { remaining: 0 } —— 剩余额度为 0 的那次已经是拒绝。
注意 record_failure(计数)与 ban(封禁)是两次独立的锁获取,不原子:
两者之间并发一次 reset 是可能的,结果是刚认证成功的用户又被封上
(可用性问题,不构成绕过 —— 计数也确实已经记下了)。若那个窗口不可接受,
得把「计数 + 判阈值 + 写封禁」并成一个 store 操作。
同理,ban 写失败时计数已经落库:本调用返回 Err,但下一步 check
会看到 Allow { remaining: 0 }(额度确实耗尽),由调用方据此拒绝。
Sourcepub fn record_success(&self, key: &str) -> Result<(), StoreError>
pub fn record_success(&self, key: &str) -> Result<(), StoreError>
认证成功时调用:只清失败计数,保留封禁。
「凭据正确 ⇒ 不是暴力破解 ⇒ 顺手解封」只对 acct: 桶成立。对 ip: 这类
共享桶,封禁是 NAT / 代理后面的所有人共用的 —— 换成 reset(清计数 + 清封禁)
意味着桶里任意另一个用户认证成功,就能替爆破者解除封禁并洗掉计数。
这是有意的安全取舍,不是漏写的细节:代价是账户桶下用户被爆破牵连时,
即使立刻输对密码也要等满 ban_secs(默认 900 秒)才恢复;换来的是共享桶的
封禁不会被他人一次成功认证解除。封禁不需要在这里额外清理,is_banned 的
now 过滤会让它自然到期;要人工提前解封用 Throttle::reset。