为外部输入设置长度、格式与资源预算三层限制
来源:17golang原创
时间:2026-10-08 19:22:09 325浏览 收藏
外部输入不能只做一次“JSON 能否解析”的检查。一个稳定的 Go 接口至少需要三层彼此独立的限制:长度预算控制最多读取多少字节,格式预算控制允许出现哪些字段和值,资源预算控制一次请求最多占用多久、同时允许多少份工作进入下游。任何一层缺失,都可能让合法语法变成资源消耗入口。
推荐顺序是:先限制字节,再解码并校验结构,最后给已经通过校验的业务工作分配时间与并发额度。不要依赖客户端声明的 Content-Length,也不要把数据库超时当成请求体长度限制。
官方文档:https://pkg.go.dev/net/http#MaxBytesReader、https://pkg.go.dev/encoding/json#Decoder.DisallowUnknownFields、https://pkg.go.dev/context#WithTimeout
三层限制分别拦住什么问题
| 限制层 | 典型配置 | 主要防止的问题 | 不能替代的检查 |
|---|---|---|---|
| 长度预算 | 64 KiB 请求体 | 大请求体、无界读取、过度内存占用 | 字段是否合法 |
| 格式预算 | 未知字段拒绝、枚举、数量与字符串长度 | 拼错字段、宽松绑定、巨大数组、异常业务值 | 处理时间和并发容量 |
| 资源预算 | 2 秒超时、32 个并发槽位 | 慢请求、下游挂起、突发并发放大 | 输入体实际字节数 |
这些数字不是通用答案。64 KiB、2 秒和 32 个槽位只是示例,应该根据接口用途、正常请求分布、下游连接池和可接受失败策略确定。关键是让每个边界都可测量、可配置、可返回明确错误。
长度门禁必须放在解码器之前

http.MaxBytesReader 专门用于限制入站请求体读取量。读取超过限制时会返回 *http.MaxBytesError,便于接口稳定地映射为 413。它应包在 json.Decoder 外面,让解析器从一开始就只能看到受限的数据源。
package report
import (
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
"strings"
"unicode/utf8"
)
const (
maxBodyBytes = 64 maxNameRunes {
return CreateReportInput{}, fmt.Errorf("%w: name 长度超限", errInvalidInput)
}
if input.Format != "csv" && input.Format != "json" {
return CreateReportInput{}, fmt.Errorf("%w: format 只允许 csv 或 json", errInvalidInput)
}
if len(input.Rows) == 0 || len(input.Rows) > maxRows {
return CreateReportInput{}, fmt.Errorf("%w: rows 数量超限", errInvalidInput)
}
for index, row := range input.Rows {
if len(row) > maxRowBytes {
return CreateReportInput{}, fmt.Errorf("%w: rows[%d] 过长", errInvalidInput, index)
}
}
return input, nil
}
DisallowUnknownFields 能发现客户端把 format 错写成 formt,但它不会自动限制字符串长度、数组元素数量或枚举范围。因此严格解码和业务语义校验必须同时存在。通过校验后,应把输入映射到窄类型命令,不要继续向下游传递 map[string]any 或原始字节。
io.LimitReader 适合通用读取场景,但超过 N 字节时它会像读到 EOF 一样停止,不能直接告诉 HTTP 处理器“客户端超限”。入站请求体优先使用 MaxBytesReader,错误类型和连接处理语义更适合服务器。
时间预算与并发预算解决不同问题

超时限制一份工作最多存活多久;并发槽位限制同一时间最多存在多少份工作。只加超时,突发流量仍可能同时压垮下游;只加并发槽位,某个卡住的工作又可能长期占住容量。两者要配合使用。
package report
import (
"context"
"errors"
"net/http"
"time"
)
const workTimeout = 2 * time.Second
var reportSlots = make(chan struct{}, 32)
type ReportStore interface {
CreateReport(context.Context, CreateReportInput) error
}
func CreateReportHandler(store ReportStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
input, err := decodeCreateReport(w, r)
if err != nil {
writeInputError(w, err)
return
}
// 从请求 Context 派生总预算,客户端断开时也会同步取消。
ctx, cancel := context.WithTimeout(r.Context(), workTimeout)
defer cancel()
select {
case reportSlots
context.WithTimeout 返回的 CancelFunc 应始终调用,即使操作提前成功也要释放关联计时器。更重要的是,下游必须真正观察这个 Context:数据库使用 ExecContext、QueryContext,HTTP 客户端使用带 Context 的请求,RPC 客户端也要把截止时间继续传下去。只在处理器里创建 Context,却调用不支持取消的阻塞方法,超时不会回收实际工作。
错误码要指向真正的边界
明确区分错误有两个好处:客户端知道该修改请求还是稍后重试,服务端指标也能分清攻击面、接口兼容问题和容量问题。
- 413 Request Entity Too Large:实际读取超过请求体上限。
- 415 Unsupported Media Type:接口要求 JSON,但媒体类型不是
application/json。 - 400 Bad Request:JSON 损坏、出现未知字段、存在第二个 JSON 值或业务字段不合法。
- 503 Service Unavailable:在预算内没有拿到并发槽位,当前容量不足。
- 504 Gateway Timeout:已经进入业务处理,但依赖在截止时间内没有完成。
错误响应不要回显整段原始请求体。日志可以记录请求 ID、错误类别、限制值和实际阶段,但敏感字段需要脱敏。对于公开接口,还可以给每个身份、IP 或令牌增加速率限制;速率限制控制“多久允许多少次”,它仍不能替代单请求的长度和资源预算。
表单与文件上传还有两处常见误区
Request.ParseForm 在请求体没有被 MaxBytesReader 限制时,对某些表单路径有 10 MB 的默认上限,但这不是所有接口统一的安全策略。显式设置自己的上限更清楚,也便于按端点区分。
ParseMultipartForm(maxMemory) 中的 maxMemory 表示文件部分最多保留多少在内存,超出的部分可以写入临时文件;它不是上传总量上限。文件上传应先用 MaxBytesReader 限总字节,再调用 ParseMultipartForm 限内存,并在完成后调用 r.MultipartForm.RemoveAll() 清理临时文件。若文件会解压、解码图片或展开归档,还要限制解码后的尺寸、条目数和展开总量,网络字节数并不等于最终内存或磁盘占用。
服务器级超时是最后一道公共边界
除了处理器自己的业务期限,http.Server 还应设置 ReadHeaderTimeout、WriteTimeout、IdleTimeout 和合适的 MaxHeaderBytes。官方文档指出,很多服务更适合使用 ReadHeaderTimeout,再由处理器根据端点决定请求体读取期限。这样上传接口和小 JSON 接口不必共享一个僵硬的 body 超时。
如果需要对读取请求体本身设置更精细的截止时间,可以使用 http.ResponseController 或连接层能力,但要先明确反向代理、负载均衡器和应用服务器各自的超时顺序,避免外层已经断开、内层仍继续工作。
上线前复查清单
- 是否在任何解码、解压或落盘之前限制了真实读取字节数?
- 是否拒绝未知字段、尾随第二个 JSON 值和错误的媒体类型?
- 字符串、数组、分页大小、枚举和嵌套深度是否各有业务上限?
- 通过校验后是否映射为窄类型命令,而不是继续传递原始输入?
- Context 截止时间是否传到了数据库、HTTP、RPC 和队列客户端?
- 是否同时限制单次工作时长和整体在途并发量?
cancel、并发槽位、响应体、临时文件是否在所有返回路径上释放?- 413、415、400、503、504 是否有独立指标和不含敏感数据的日志?
三层限制的核心不是堆叠更多校验函数,而是让外部数据在生命周期的每一段都只能消费预先批准的预算:进入解析器前有字节边界,进入业务前有结构与语义边界,进入下游前有时间与容量边界。这样接口面对异常输入时,失败会更早、更便宜,也更容易定位。
-
418 收藏
-
401 收藏
-
175 收藏
-
236 收藏
-
137 收藏
-
Golang · Go教程 | 48分钟前 | CGO · 资源管理 · Go教程 · runtime.KeepAlive runtime/cgo.Handle Go cgo C库句柄 LockOSThread400 收藏
-
244 收藏
-
283 收藏
-
228 收藏
-
382 收藏
-
474 收藏
-
427 收藏
-
243 收藏
-
332 收藏
-
245 收藏
-
447 收藏
-
207 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习