95 lines
2.5 KiB
Go
95 lines
2.5 KiB
Go
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)
|