From d136c238747d55cfd23fa816a8f9b783588d7c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=8A=80=E9=BA=9F?= Date: Mon, 14 Sep 2026 02:33:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(encrypt):=20=E5=85=A8=E9=87=8F=E5=8A=A0?= =?UTF-8?q?=E5=AF=86=E8=B1=81=E5=85=8D=20multipart=20=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E8=A2=AB=E6=8B=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:encrypt.fullBody=true 时,APICrypto 中间件会把所有非豁免请求的 body 当 JSON 解析取 {encryptedKey, encryptedData}。文件上传走 multipart/form-data,body 是二进制流,json.Unmarshal 必然失败,于是 被判定为「未加密的业务请求」直接拒绝(encrypted request required)。 而前端的请求拦截器对 FormData 是一律跳过加密的 (admin request.ts:`config.data instanceof FormData` 直接 return), 即上传请求本就以明文 multipart 发出。两端语义不一致,导致 fullBody 一开启,后台壁纸上传必然报错。 修复:新增 isMultipartRequest 判断,命中 multipart/form-data 的请求 跳过整体解密;响应侧因拿不到会话密钥(aesKeyB64 为空),保持明文 回传,与前端「FormData 请求不解密响应」的行为对齐。 dev 环境 fullBody=false,该路径不会被触发,故此前联调未暴露。 上传接口仍受 JWT 鉴权与 RBAC(POST + path 精确匹配)保护,豁免 整体加密不降低鉴权强度。 --- internal/middleware/encrypt.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/internal/middleware/encrypt.go b/internal/middleware/encrypt.go index fc908d5..de989b5 100644 --- a/internal/middleware/encrypt.go +++ b/internal/middleware/encrypt.go @@ -42,6 +42,20 @@ func isPlainRoute(r *ghttp.Request) bool { return false } +// isMultipartRequest 判断是否为 multipart/form-data 请求(文件上传)。 +// +// 必须豁免的原因:上传体是二进制文件流,无法整体做 JSON 加解密;前端的 +// 请求拦截器对 FormData 一律跳过加密(见 admin 的 request.ts: +// `config.data instanceof FormData` 直接 return),两端必须保持一致, +// 否则上传会被当成「未加密的业务请求」直接拒绝, +// 表现为「选了文件、点了上传,报 encrypted request required」。 +// +// 豁免不影响安全:上传接口同样挂在 /api/service/admin 分组下, +// 仍受 JWT 鉴权与 RBAC 菜单权限(POST + path 精确匹配)双重保护。 +func isMultipartRequest(r *ghttp.Request) bool { + return gstr.Contains(r.Header.Get("Content-Type"), "multipart/form-data") +} + // APICrypto 全量请求/响应加密中间件。 // 必须注册在 ghttp.MiddlewareHandlerResponse 外层(先于其执行), // 且建议置于 Recover 外层,使 500 等异常响应同样被加密。 @@ -49,7 +63,9 @@ func isPlainRoute(r *ghttp.Request) bool { func APICrypto(svc *cryptolib.Service) ghttp.HandlerFunc { return func(r *ghttp.Request) { // ---- 请求解密 ---- - if svc.FullBody() && !isPlainRoute(r) { + // multipart/form-data(文件上传)整体豁免:二进制流无法 JSON 加密, + // 前端对 FormData 同样跳过加密,此处必须同步放行。 + if svc.FullBody() && !isPlainRoute(r) && !isMultipartRequest(r) { // 直接读原始 body(勿用 r.GetBody():会缓存密文到 bodyContent, // 而 handler 的 Parse 走 GetBody 缓存,导致收到密文而非解密后的明文)。 body, _ := io.ReadAll(r.Request.Body)