中间件中需用 io.readall 读取并重置 request.body,否则后续无法读取;aes-gcm 解密须校验 nonce、aad 和密钥一致性,失败时 abort 且不暴露细节;仅对 x-encrypted 标记或特定 content-type 的请求解密;响应加密应在 handler 层显式处理。

中间件里怎么拿到原始 POST 请求体?
直接用 c.PostForm 或 c.ShouldBindJSON 会失败——因为 Gin 默认读取一次 Request.Body 后就关闭了,后续再读就是空。必须在中间件里提前“偷读”并重置 Body。
正确做法是:用 io.ReadAll(c.Request.Body) 拿到原始字节,再用 io.NopCloser 包装回去,最后调用 c.Request.Body = newBody。否则下游 handler 会收不到数据。
- 别漏掉
defer c.Request.Body.Close(),否则连接可能卡住 - 如果请求体很大(比如文件上传),别全读进内存,应改用流式解密(见下文)
-
c.Request.ContentLength == -1表示 Transfer-Encoding: chunked,此时ReadAll仍可用,但要注意超时
如何在中间件中安全执行 AES-GCM 解密?
解密逻辑不能写死密钥,也不能用 ParseUnverified 那类跳过校验的函数。Gin 中间件本身不提供加解密能力,你得自己调用 crypto/aes + cipher.NewGCM。
关键点在于:解密失败必须 abort,且错误不能暴露细节(比如别返回 cipher: message authentication failed 这种原生错误)。
- 密钥从
os.Getenv("AES_KEY")或 secret store 加载,长度必须是 16/24/32 字节 - GCM 的 nonce 是 12 字节,必须从密文前缀提取,不能硬编码
- 解密后建议验证 JSON 结构(如用
json.Valid),防止篡改后引发 panic - 若解密成功,把明文写回
c.Request.Body,并设置c.Set("decrypted", true)供后续 handler 确认
GET 和 multipart/form-data 怎么统一处理?
不是所有请求都走 JSON body。GET 参数在 URL query 里,form-data 有 boundary,它们没法用同一套解密逻辑。
实际策略是:只对明确标记加密的请求解密。比如约定 Header X-Encrypted: aes-gcm,或 Content-Type 为 application/encrypted+json。没这个标记的请求跳过解密。
- GET 请求体为空,加密参数只能放在 query 或 header,中间件应检查
c.Request.URL.Query().Get("data")并解密 - multipart/form-data 要先解析 boundary,找到目标字段(如
payload),再对其值解密;不能整块解密,会破坏 boundary - 别试图在中间件里自动识别“看起来像密文”的字符串(比如 base64 长度 + 特征字节),极易误判
为什么不能在中间件里修改响应体做“加密返回”?
因为 c.Writer 是写入接口,不是缓冲区。你无法在 c.Next() 之后“读出”已写的响应内容再加密——它可能已经发给客户端了,尤其是流式响应(如 SSE、大文件下载)。
真正可行的方式只有两种:要么用 gin.ResponseWriter 包装器拦截 Write 方法,在每次写入时加密;要么让业务 handler 自己调用加密函数,把加密结果传给 c.JSON。
- 包装器方式要小心
WriteHeader和Write的顺序,HTTP 状态码必须先发 - 更推荐 handler 层显式加密:定义
EncryptResponse(data interface{}) ([]byte, error),业务侧调用后直接c.Data(200, "application/encrypted+json", encrypted) - 如果必须统一出口,包装器里只加密
Content-Type: application/json响应,其他类型(如text/html)直接 passthrough
最常被忽略的是:加密和解密必须用完全相同的 key、nonce 生成逻辑、AEAD 附加数据(AAD)。哪怕 AAD 少传一个 header 字段,Open 就会失败。别指望靠日志猜问题——把 key、nonce、AAD 的构造过程打在 debug 日志里,但上线后关掉。











