rate.Limiter必须复用且按IP、用户ID等维度分桶共享实例,不能每请求新建;应优先使用Wait(ctx)配合上下文超时控制,避免Allow()导致超限;需结合sync.Map缓存并定期清理key,防止泛滥。

为什么 http.HandlerFunc 里直接加计数器会失效
因为 Go 的 HTTP handler 是并发执行的,多个请求同时到达时,共享变量(比如一个全局 int)不做同步就会出现竞态,计数值严重不准。更麻烦的是,重启服务后计数清零,但攻击者可能已经绕过阈值。
实操建议:
- 用
sync.Map或sync.RWMutex保护客户端维度的计数,键推荐用IP + User-Agent组合(防代理池),不要只用 IP - 必须搭配 TTL,比如用
time.Now().Unix()作为滑动窗口起点,每分钟重置一次计数 —— 否则累计值永远涨,无法实现“每分钟最多 10 次”这种语义 - 别在 handler 里写
if count > 10 { return }就完事,要提前在中间件里拦截并返回429 Too Many Requests,否则上传体(尤其是大文件)已开始读取,浪费带宽和内存
如何用 golang.org/x/time/rate 做平滑限流
标准库的 rate.Limiter 比手动计数更可靠,它基于令牌桶,支持突发流量容忍,且自带并发安全。但注意:它默认不区分客户端,必须为每个 IP 构建独立 *rate.Limiter 实例,否则全站共用一个桶就变成全局限流了。
实操建议:
- 用
sync.Map缓存每个clientKey对应的*rate.Limiter,初始化时传入rate.Every(1 * time.Minute)和burst=10 - 每次请求调用
limiter.AllowN(time.Now(), 1)判断是否放行;别用WaitN,上传接口不能阻塞等待 - 注意内存泄漏:定期清理长时间未活跃的 key,比如用后台 goroutine 每 5 分钟扫描
sync.Map,删除最后访问时间早于 30 分钟前的条目
上传体读取前就得完成鉴权与限流
如果先调用 r.ParseMultipartForm(32 再检查限流,攻击者发一个 100MB 的恶意 multipart/form-data,Go 会先把整个 body 解析进内存或临时磁盘,服务可能 OOM 或 I/O 打满。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
实操建议:
- 在
http.Request.Body被消费前,用io.LimitReader(r.Body, maxHeaderSize)先读取 header 部分(含Content-Length),超过阈值(如 50MB)直接拒绝 - 用
mime.Reader检查Content-Type是否为multipart/form-data,不是就 400;是的话再解析 boundary 并校验第一个Part的Content-Disposition字段是否含name="file" - 所有限流、JWT 校验、文件名黑名单过滤,都必须在这一步之后、
r.MultipartReader()之前完成
net/http 默认配置对上传攻击毫无抵抗力
Go 的 http.Server 默认不限制请求头大小、body 大小、连接超时,攻击者发大量小文件请求(每秒上百个 1KB 文件)就能轻易耗尽 goroutine 和 fd。
实操建议:
- 设置
ReadTimeout: 5 * time.Second、WriteTimeout: 30 * time.Second,防止慢速攻击 - 用
MaxHeaderBytes = 1 (1MB)限制 header 大小,避免 CRLF 注入或超长字段 - 关键:通过
http.MaxBytesReader包裹原始Body,例如http.MaxBytesReader(w, r.Body, 50,强制在读取阶段截断超大 body
真正难处理的是混合场景:既要允许单个 200MB 视频上传,又要防住 1000 个并发的 1MB 垃圾文件刷接口。这时候光靠单机限流不够,得在反向代理层(如 Nginx)做连接数限制,再配合 Go 层按用户 ID 做二级限流 —— 但那是另一个需要单独评估漏斗深度的问题了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










