登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

为外部输入设置长度、格式与资源预算三层限制

来源: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 个槽位只是示例,应该根据接口用途、正常请求分布、下游连接池和可接受失败策略确定。关键是让每个边界都可测量、可配置、可返回明确错误。

长度门禁必须放在解码器之前

请求体、MaxBytesReader、JSON 解码器、结构体和业务校验的静态边界关系
图1:长度与格式边界说明图。字节上限先于 JSON 解码,结构绑定之后还要执行独立的业务语义校验。

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,错误类型和连接处理语义更适合服务器。

时间预算与并发预算解决不同问题

请求 Context、超时、并发槽位、存储接口和下游 Context 方法的静态关系
图2:资源预算说明图。超时限制单次工作存活时间,并发槽位限制同时在途数量,两者都要传递或配对释放。

超时限制一份工作最多存活多久;并发槽位限制同一时间最多存在多少份工作。只加超时,突发流量仍可能同时压垮下游;只加并发槽位,某个卡住的工作又可能长期占住容量。两者要配合使用。

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 是否有独立指标和不含敏感数据的日志?

三层限制的核心不是堆叠更多校验函数,而是让外部数据在生命周期的每一段都只能消费预先批准的预算:进入解析器前有字节边界,进入业务前有结构与语义边界,进入下游前有时间与容量边界。这样接口面对异常输入时,失败会更早、更便宜,也更容易定位。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>