解密必须在中间件中手动实现,Gin框架不自动解密;需用r.URL.RawQuery解析密文query,或在r.ParseForm/ShouldBindJSON前用io.ReadAll读取并重置r.Body,且解密后须校验PKCS#7填充和UTF-8有效性,并正确重置r.Body与ContentLength。

解密必须在中间件里手动做,框架不自动解
Gin 不会帮你解密任何参数,r.URL.Query() 和 r.PostFormValue() 都是“解析后”的结果,密文早被破坏了。你看到的乱码或空值,大概率是因为前端传的是 base64 编码的密文(比如 SGVsbG8%3D),而 r.URL.Query() 内部调用了 url.QueryUnescape,把 %3D 变成 =,再交给 base64.DecodeString 就失败。
正确入口只有两个:
- query 解密:用
r.URL.RawQuery拿到原始字符串,再用url.ParseQuery手动解析,保留原始编码 - body 解密:必须在调用
r.ParseForm()或c.ShouldBindJSON()之前,先用io.ReadAll(r.Body)读一次,解密后再写回r.Body(需用io.NopCloser包装)
query 和 body 解密不能混用
微信小程序的 encryptedData 通常走 POST body,而风控接口可能把密文塞在 query 里(如 ?payload=xxx)。二者生命周期完全不同:
- query 是 URL 字符串的一部分,生命周期长,可反复解析
- body 是流式数据,
r.Body只能读一次;一旦调过r.FormValue()或c.ShouldBindJSON(),内部就触发了r.ParseForm()或io.ReadAll,后续再读就是nil或io.EOF
常见错误:在中间件里先调 c.ShouldBindJSON(&v),再想解密 body —— 此时 r.Body 已空,解密无从谈起。
立即学习“go语言免费学习笔记(深入)”;
AES-CBC 解密后必须校验 UTF-8 和 PKCS#7 填充
解密失败时,cipher.CryptBlocks 返回的可能是随机字节,直接转 string 后传给 json.Unmarshal,遇到非法 UTF-8 会 panic。所以 PKCS7UnPadding 不是可选步骤,而是安全底线。
示例关键判断:
plaintext, err := PKCS7UnPadding(decrypted)
if err != nil {
return "", err // 不能忽略
}
if !utf8.Valid(plaintext) {
return "", errors.New("decrypted data is not valid UTF-8")
}
注意:PKCS7UnPadding 中的 unpadding := int(data[length-1]) 必须 ≤ length,否则越界 panic —— 这个检查不能省。
Gin 中间件里重写 r.Body 的坑
解密完 body 后,得让后续 handler 还能正常调 c.ShouldBindJSON() 或 r.ParseForm()。不能只解密完就完事,必须把解密后的字节重新塞回 r.Body:
- 别直接赋值
r.Body = bytes.NewReader(decrypted)—— 这会导致r.ContentLength不匹配,某些 handler(如文件上传)会出错 - 正确做法:用
io.NopCloser(bytes.NewReader(decrypted)),并手动设置r.ContentLength = int64(len(decrypted)) - 如果原请求是 multipart/form-data,解密后还必须重建
r.MultipartForm,否则r.FormFile()失效
最易忽略的一点:中间件执行顺序。如果你注册了多个中间件(比如日志、JWT、解密),解密中间件必须在 JWT 验签之前,否则验签用的是密文而非明文。


















