Gin 的 BodySizeLimit 配置常失效,因其本质是调用 http.MaxBytesReader,必须在任何 r.Body 读取操作(如 r.ParseForm()、io.ReadAll)前完成包装;若中间件或 handler 提前消费 body,则限流无效。

为什么 Gin 的 BodySizeLimit 配置经常失效
它只是对 r.Body 做一次包装,但必须在任何读取操作之前生效。一旦中间件(比如 JWT 解析、日志记录)调用了 r.ParseForm() 或 io.ReadAll(r.Body),原始 body 就被提前消费掉了,后续的限流包装就形同虚设。
- 常见漏点:自定义鉴权中间件里调了
c.Request.ParseForm(),却没意识到这会清空 body - 业务 handler 里绕过
c.ShouldBindJSON(),直接用json.NewDecoder(c.Request.Body).Decode(),也没手动套http.MaxBytesReader -
gin.DefaultWriter在 v1.9+ 默认启用限流,但如果你自己 new 了一个gin.Engine实例,这个默认行为可能被覆盖
全局请求体大小限制该用 MaxBytesHandler 还是 MaxBytesReader
http.MaxBytesHandler 是最稳妥的全局拦截方式——它在请求进入路由前就检查总长度,超限直接返回 413 并关闭连接,不进任何 handler,也不触发中间件逻辑。但缺点是所有接口共用一个上限,不适合上传接口和登录接口混用的场景。
http.MaxBytesReader 是 per-handler 控制,需要你在每个 handler 开头手动赋值:r.Body = http.MaxBytesReader(w, r.Body, 50。灵活,但容易漏写或顺序错(必须在任何 <code>r.Body 读取之前)。
- 推荐组合:用
MaxBytesHandler拦住明显恶意的大请求(如 >100MB),再在关键 handler 里用MaxBytesReader做细粒度控制(如上传接口限 50MB,API 接口限 2MB) - 注意:如果用了
ParseMultipartForm,它的maxMemory参数只管内存缓存部分,不是总大小——必须前置用MaxBytesReader卡死总上传量
用 rate.Limiter 做每秒请求数限制时最容易踩的坑
直接写 time.Sleep 完全无效,它只阻塞当前 goroutine,对并发请求毫无约束力。真正要的是共享状态 + 原子判断。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 别用全局
sync.Mutex锁计数器:高并发下锁争用严重,QPS 直线下跌 - 别把计数存在裸 map 里不清理:key 不过期,内存持续增长
- 别对所有路径统一用一个限流器:登录接口和公开文档接口不该互相拖累
- 正确做法是按
r.URL.Path或用户 ID 分 key 创建rate.Limiter,用sync.Map缓存实例
上传接口防木马图片攻击的关键配置点
Gin 本身不校验文件内容,仅靠扩展名或 MIME 类型很容易被绕过。必须结合多层防护:
- 设置
router.MaxMultipartMemory = 8 (8MB),让小文件走内存,大文件落磁盘,但前提是已用 <code>MaxBytesReader限制总上传体积 - 调用
c.FormFile("file")获取multipart.FileHeader,不要用c.PostForm("file")——后者只读文本字段 - 保存前用
file.Open()读取前几百字节,校验 magic bytes(如 PNG 是89 50 4E 47,JPG 是FF D8 FF),拒绝非预期二进制头 - 生成唯一文件名,禁用用户传入的原始文件名,防止路径遍历(如
../../etc/passwd)
限流和防刷不是加个中间件就完事的事——顺序、作用域、数据生命周期,每一步都得对得上。尤其注意那些“看似生效、实则被绕过”的配置,它们比完全没配更危险。

















