service.xpcool.com/internal/middleware/encrypt.go
夏犀麟 ebc507ee6c feat(auth): 生产全量请求/响应加密中间件
- crypto.Service 增 fullBody 开关 + DecryptRequest/EncryptResponse(会话密钥双向加密)
- 新增 middleware/APICrypto:请求整体解密 + 响应加密(public-key 豁免),未加密请求拒绝
- ResolveLogin/resolvePassword 增 fullBody 分支;admin 组中间件链调整
- injectEnv 支持 ENCRYPT_FULL_BODY/ENCRYPT_ALLOW_PLAIN(直接跑二进制需 env 注入)
2026-08-28 00:17:47 +08:00

95 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 全量请求/响应加密中间件(生产环境 encrypt.fullBody=true 时启用)。
//
// 传输模型(对称+非对称结合):
// - 请求:前端随机 AES-256 密钥 → RSA-OAEP 公钥加密密钥encryptedKey+
// AES-GCM 加密整个业务 JSON bodyencryptedData含 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)
}
}
}