// Package ratelimit 是一个极小的按来源 IP 滑动窗口限速器, // 后台登录与读者登录 / 评论写入共用同一实现。 // 只计失败/写入这类「不该高频」的事件;键取连接层地址, // X-Forwarded-For 可伪造,不作为限速键。 package ratelimit import ( "net" "net/http" "sync" "time" ) type Window struct { mu sync.Mutex hits map[string][]time.Time max int window time.Duration } func New(max int, window time.Duration) *Window { return &Window{hits: make(map[string][]time.Time), max: max, window: window} } // Blocked 报告 key 在窗口内是否已达上限。 func (l *Window) Blocked(key string) bool { l.mu.Lock() defer l.mu.Unlock() return len(l.recent(key, time.Now())) >= l.max } // Add 记录一次事件。 func (l *Window) Add(key string) { l.mu.Lock() defer l.mu.Unlock() now := time.Now() l.hits[key] = append(l.recent(key, now), now) } // Reset 清空 key 的计数(如登录成功后)。 func (l *Window) Reset(key string) { l.mu.Lock() defer l.mu.Unlock() delete(l.hits, key) } // recent 返回窗口内的事件时间;调用方必须持有 l.mu。 func (l *Window) recent(key string, now time.Time) []time.Time { hs := l.hits[key] cut := now.Add(-l.window) i := 0 for ; i < len(hs); i++ { if hs[i].After(cut) { break } } if i > 0 { hs = hs[i:] l.hits[key] = hs } if len(hs) == 0 { delete(l.hits, key) } return hs } // SourceKey 取连接层来源地址(IPv6 去掉端口)。 func SourceKey(r *http.Request) string { if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { return host } return r.RemoteAddr }