rate.limiter需按ip复用,用sync.rwmutex保护map[string]*rate.limiter并提取纯ip;长连接须在accept阶段限连;http服务器须显式设read/write/idletimeout;上传接口必须用maxbytesreader限制body大小。

rate.Limiter 必须按 IP 复用,不能全局单例
全局共用一个 rate.Limiter 实际上是把所有 IP 的请求塞进同一个桶,等于完全没限流。攻击者只要换几个 IP 就能绕过——而真实场景中,同一 IP 可能带端口(如 192.168.1.100:54321),直接拿 r.RemoteAddr 当 key 会导致同一客户端被识别为多个不同 IP。
正确做法是:用 sync.RWMutex 保护 map[string]*rate.Limiter,提取纯 IP 时调用 strings.Split(r.RemoteAddr, ":")[0];敏感路径(如登录)用 rate.NewLimiter(2, 3),普通 API 可设为 (10, 20);lim.Allow() 返回 false 时立刻 http.Error(w, "", http.StatusTooManyRequests),不进业务逻辑。
长连接服务必须在 Accept 阶段拒连,不能等握手完成
RTMP、WebSocket 这类协议,攻击者建 1000 个空连接就能耗尽 net.Conn 和 goroutine,此时 HTTP 层限流完全失效。Livego 的 max_connections: 1000 仅用于内部统计,不自动拒绝新连接。
必须在 net.Listener.Accept() 返回后立即检查活跃连接数:
- 用
atomic.Int64存当前连接数,避免sync.WaitGroup在万级并发下锁争抢 - 超限时直接
conn.Close()并记录日志,不走后续协议解析 - 参考位置:Livego 的
protocol/rtmp/accept.go或自研信令服务的 accept loop
HTTP Server 超时必须显式设三值,IdleTimeout 尤其关键
IdleTimeout 不设默认为 0(永不超时),Slowloris 类攻击可长期占着连接不发数据。若 Nginx 的 keepalive_timeout 是 60s,Go 的 IdleTimeout 必须设为更小值(如 30s),否则连接会卡在 Go 层不释放。
三项必须显式配置:
-
ReadTimeout: 5 * time.Second—— 防慢速读(如 Slowloris) -
WriteTimeout: 10 * time.Second—— 防响应慢导致连接堆积 -
IdleTimeout: 30 * time.Second—— 防空闲连接长期占用
对非浏览器客户端(IoT、爬虫),还可加 w.Header().Set("Connection", "close") 禁用 Keep-Alive。
上传接口必须用 MaxBytesReader 包装 Body
不加约束的大 payload(比如 500MB 文件上传)会触发 GC 风暴甚至 OOM,且攻击者可用极小速率发送大 body 拖垮服务。
正确姿势是:在 handler 开头就包装 r.Body:
limitReader := http.MaxBytesReader(w, r.Body, 5*1024*1024) // 5MB
body, err := io.ReadAll(limitReader)
if err == http.ErrBodyReadAfterClose {
return
}
if err != nil && errors.Is(err, http.ErrContentLength) {
http.Error(w, "Payload too large", http.StatusRequestEntityTooLarge)
return
}
注意:这个限制只作用于 r.Body,不影响 header 大小;若需控 header,得靠反向代理(如 Nginx)或自定义 http.ReadHeaderTimeout。
真正难的不是写几行限流代码,而是判断哪道防线该放在哪一层——Accept 阶段的连接数检查、HTTP 层的 per-IP 令牌桶、Nginx 层的连接数限制,三者位置错一位,防护效果就断层。别指望 Go 应用自己扛住 UDP Flood 或 SYN Flood,它只该管住“连接生命周期”“请求体约束”“关键路径熔断”这三件事。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











