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)