// 全量请求/响应加密中间件(生产环境 encrypt.fullBody=true 时启用)。 // // 传输模型(对称+非对称结合): // - 请求:前端随机 AES-256 密钥 → RSA-OAEP 公钥加密密钥(encryptedKey)+ // AES-GCM 加密整个业务 JSON body(encryptedData,含 nonce/tag)→ 后端私钥解出密钥与明文。 // - 响应:后端用同一 AES 会话密钥加密响应 JSON,回传 {encryptedData},前端持密钥解密。 // // 会话密钥生命周期:请求解密时生成/提取 → 存入请求上下文 → 响应加密时取出使用,请求结束即销毁。 // 豁免路由:public-key(必须先明文拿到公钥才能发起加密请求)。 package middleware import ( "bytes" "encoding/json" "io" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/text/gstr" "service.xpcool.com/internal/consts" cryptolib "service.xpcool.com/internal/library/crypto" "service.xpcool.com/internal/library/response" ) // aesKeyCtxVar 请求上下文中 AES 会话密钥(base64 字符串)的键名。 const aesKeyCtxVar = "__crypto_aes_key" // plainRoutes 全量加密豁免路由(path 片段匹配),此类端点明文收发。 var plainRoutes = []string{"/public-key"} // isPlainRoute 判断当前请求是否属于明文豁免端点。 func isPlainRoute(r *ghttp.Request) bool { for _, frag := range plainRoutes { if gstr.Contains(r.URL.Path, frag) { return true } } return false } // APICrypto 全量请求/响应加密中间件。 // 必须注册在 ghttp.MiddlewareHandlerResponse 外层(先于其执行), // 且建议置于 Recover 外层,使 500 等异常响应同样被加密。 // 开发环境(fullBody=false)该中间件为空操作,仅保留密码字段加密。 func APICrypto(svc *cryptolib.Service) ghttp.HandlerFunc { return func(r *ghttp.Request) { // ---- 请求解密 ---- if svc.FullBody() && !isPlainRoute(r) { // 直接读原始 body(勿用 r.GetBody():会缓存密文到 bodyContent, // 而 handler 的 Parse 走 GetBody 缓存,导致收到密文而非解密后的明文)。 body, _ := io.ReadAll(r.Request.Body) r.Request.Body = io.NopCloser(bytes.NewReader(body)) if len(body) > 0 { var req struct { EncryptedKey string `json:"encryptedKey"` EncryptedData string `json:"encryptedData"` } if err := json.Unmarshal(body, &req); err != nil || req.EncryptedKey == "" || req.EncryptedData == "" { // 生产强制全量加密:未加密的业务请求一律拒绝。 response.JSON(r, consts.CodeInvalidParam, "encrypted request required", nil) return } aesKeyB64, plain, err := svc.DecryptRequest(req.EncryptedKey, req.EncryptedData) if err != nil { response.JSON(r, consts.CodeInvalidParam, "request decrypt failed", nil) return } // 替换请求体供 handler 解析,并暂存会话密钥供响应加密。 r.Request.Body = io.NopCloser(bytes.NewReader(plain)) r.SetCtxVar(aesKeyCtxVar, aesKeyB64) } } // ---- 执行业务链(含认证中间件与 HandlerResponse 响应写出) ---- r.Middleware.Next() // ---- 响应加密 ---- if svc.FullBody() && r.Response.BufferLength() > 0 { buf := r.Response.Buffer() r.Response.ClearBuffer() aesKeyB64 := r.GetCtxVar(aesKeyCtxVar).String() if aesKeyB64 != "" { if enc, err := svc.EncryptResponse(aesKeyB64, buf); err == nil { // 客户端持有同一会话密钥,仅需回传密文。 r.Response.WriteJson(g.Map{"encryptedData": enc}) return } } // 无会话密钥(如 public-key 明文端点)或加密失败:原样输出。 r.Response.Write(buf) } } }