0904
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DuplicateBlocker 阻止同一来源(IP + 内容指纹)在窗口期内的重复提交,
|
||||
// 用于拦截刷屏 / 重复轰炸提问。
|
||||
//
|
||||
// 实现:内存 map,key = normalizedIP + "|" + contentHash,值为首次提交时间。
|
||||
// 窗口期内再次提交相同内容返回 false(拒绝)。
|
||||
//
|
||||
// 内存安全:条数超过 50000 时先清理过期项;窗口外的 key 由后台清理循环定时移除。
|
||||
type DuplicateBlocker struct {
|
||||
mu sync.Mutex
|
||||
items map[string]time.Time
|
||||
window time.Duration
|
||||
maxItem int
|
||||
}
|
||||
|
||||
// NewDuplicateBlocker 创建拦截器并启动后台清理循环。
|
||||
// 注意:每个实例会启动一个常驻 goroutine,应用内应使用包级单例 DuplicateGuard,
|
||||
// 不要为每次请求创建新实例。
|
||||
func NewDuplicateBlocker(window time.Duration) *DuplicateBlocker {
|
||||
d := &DuplicateBlocker{
|
||||
items: make(map[string]time.Time),
|
||||
window: window,
|
||||
maxItem: 50000,
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(10 * time.Minute)
|
||||
d.cleanup()
|
||||
}
|
||||
}()
|
||||
return d
|
||||
}
|
||||
|
||||
// Allow 判断 key(IP+内容)是否允许提交:
|
||||
// - 窗口期内已存在相同 key → 返回 false(重复,拒绝)
|
||||
// - 否则记录并返回 true
|
||||
func (d *DuplicateBlocker) Allow(key string) bool {
|
||||
now := time.Now()
|
||||
cutoff := now.Add(-d.window)
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if t, ok := d.items[key]; ok && t.After(cutoff) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 超限时先清理过期项,避免恶意构造大量 key 撑爆内存
|
||||
if len(d.items) >= d.maxItem {
|
||||
for k, t := range d.items {
|
||||
if !t.After(cutoff) {
|
||||
delete(d.items, k)
|
||||
}
|
||||
}
|
||||
// 仍超限则整体放弃写入(宁可放行也不撑爆内存)
|
||||
if len(d.items) >= d.maxItem {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
d.items[key] = now
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *DuplicateBlocker) cleanup() {
|
||||
cutoff := time.Now().Add(-d.window)
|
||||
d.mu.Lock()
|
||||
for k, t := range d.items {
|
||||
if !t.After(cutoff) {
|
||||
delete(d.items, k)
|
||||
}
|
||||
}
|
||||
d.mu.Unlock()
|
||||
}
|
||||
|
||||
// DuplicateKey 生成拦截 key:规范化 IP + 内容 MD5。
|
||||
func DuplicateKey(ip, content string) string {
|
||||
ip = NormalizeIP(ip)
|
||||
sum := md5.Sum([]byte(strings.TrimSpace(content)))
|
||||
return ip + "|" + hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// DuplicateGuard 全局限流实例:同 IP 在 5 分钟内提交相同内容会被拒绝。
|
||||
// 阈值合理:正常用户几乎不可能在 5 分钟内提交两条完全一致的提问。
|
||||
var DuplicateGuard = NewDuplicateBlocker(5 * time.Minute)
|
||||
Reference in New Issue
Block a user