fix(ratelimit): 强制清理按最旧活跃时间排序,避免随机删活跃用户
原版用 'for k := range rl.requests' 直接删除超限条目,Go map 遍历顺序 随机,可能命中 timestamp 仍在窗口内的活跃用户,导致其下次请求拿到新配额 (限流语义被破坏)。 同时存在 abuse vector: 攻击者用大量伪造 IP 撑爆 map 触发清理,反而 '清洗'掉自己留的活跃条目,绕过限流。 修复: - 收集 (key, lastTs) 到 slice,按 lastTs 升序排(最旧在前) - lastTs 取 timestamps 末尾(Allow 总 append 到末尾,即最近请求时间) - 从前往后逐个 delete,直到 len(rl.requests) <= MaxRateLimiterEntries/2 - 阈值/触发条件/warning 日志保持不变 复杂度 O(n log n),仅在超 MaxRateLimiterEntries(10w)时触发,代价可接受。 cleanup 仍在 Allow 入口的 rl.mutex.Lock() 区域内,无需额外锁。
This commit is contained in:
+24
-2
@@ -5,6 +5,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -128,11 +129,32 @@ func (rl *RateLimiter) cleanup() {
|
|||||||
|
|
||||||
if len(rl.requests) > common.MaxRateLimiterEntries {
|
if len(rl.requests) > common.MaxRateLimiterEntries {
|
||||||
log.Printf("警告: 限流器条目数 %d 超过上限 %d,触发强制清理", len(rl.requests), common.MaxRateLimiterEntries)
|
log.Printf("警告: 限流器条目数 %d 超过上限 %d,触发强制清理", len(rl.requests), common.MaxRateLimiterEntries)
|
||||||
for k := range rl.requests {
|
// 【修复】原版用 `for k := range rl.requests` 删,Go map 遍历顺序随机,
|
||||||
|
// 会随机删掉活跃用户(其条目 timestamp 仍在窗口内),导致该用户下次请求
|
||||||
|
// 拿到新配额 — 攻击者可用大量伪造 IP 撑爆 map 触发清理,反而"清洗"
|
||||||
|
// 自己留的活跃条目,绕过限流。
|
||||||
|
// 修复:按"最近一次请求时间(lastTs)"升序排序,删最旧的(最可能已离开/低频),
|
||||||
|
// 保留最活跃用户,语义符合"限流器只淘汰冷条目"的预期。
|
||||||
|
// 排序复杂度 O(n log n),但只在超 10w 条目时触发,代价可接受。
|
||||||
|
type entry struct {
|
||||||
|
key string
|
||||||
|
lastTs time.Time
|
||||||
|
}
|
||||||
|
entries := make([]entry, 0, len(rl.requests))
|
||||||
|
for k, v := range rl.requests {
|
||||||
|
// 走到这里 v 一定非空(cleanup 第一阶段会把空 timestamps 删掉),
|
||||||
|
// 取最后一个 timestamp 作为"最近活跃时间"。
|
||||||
|
lastTs := v[len(v)-1]
|
||||||
|
entries = append(entries, entry{key: k, lastTs: lastTs})
|
||||||
|
}
|
||||||
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
|
return entries[i].lastTs.Before(entries[j].lastTs)
|
||||||
|
})
|
||||||
|
for _, e := range entries {
|
||||||
if len(rl.requests) <= common.MaxRateLimiterEntries/2 {
|
if len(rl.requests) <= common.MaxRateLimiterEntries/2 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
delete(rl.requests, k)
|
delete(rl.requests, e.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user