From 70cb86176ef571e9c29d5d2add0f53ee4c8c7c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=8A=80=E9=BA=9F?= Date: Fri, 28 Aug 2026 00:28:58 +0800 Subject: [PATCH] =?UTF-8?q?chore(auth):=20=E7=99=BB=E5=BD=95=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E8=B1=81=E5=85=8D=E5=85=A8=E9=87=8F=E5=8A=A0=E5=AF=86?= =?UTF-8?q?=EF=BC=8C=E4=BB=85=E5=8A=A0=E5=AF=86=E5=AF=86=E7=A0=81=E5=AD=97?= =?UTF-8?q?=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - APICrypto plainRoutes 增 /system/auth/login(登录请求不整体加密) - ResolveLogin 密码字段密文优先解密,兼容全量模式 --- .workbuddy/memory/CHANGELOG.md | 2 ++ internal/middleware/encrypt.go | 7 +++++-- internal/service/admin/admin/login/login.go | 21 +++++++++++---------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.workbuddy/memory/CHANGELOG.md b/.workbuddy/memory/CHANGELOG.md index 9418047..b4f605a 100644 --- a/.workbuddy/memory/CHANGELOG.md +++ b/.workbuddy/memory/CHANGELOG.md @@ -1,6 +1,8 @@ # service.xpcool.com 变更记录 > 倒序:最新在上。格式:YYYY-MM-DD | 类型 | 摘要 +2026-08-27 | CHG | 登录接口从全量加密豁免,永远只加密密码字段:APICrypto plainRoutes 增 /system/auth/login(登录请求不整体加密,handler 直接收 {username, encryptedKey, encryptedData},登录响应因无会话密钥保持明文);ResolveLogin 分支顺序调整——密码字段密文优先解密,其次 fullBody 明文、开发 allowPlain。全量模式验证 5 项(登录明文响应/info 密文/解密成功/未加密拒绝/登出)+ dev 回归 15 项全过 + 2026-08-27 | FEAT | 全量请求/响应加密(生产 encrypt.fullBody=true / env ENCRYPT_FULL_BODY):①crypto.Service 增 fullBody 开关 + DecryptRequest(RSA 解 AES 会话密钥 + AES-GCM 解 body,返回会话密钥)+ EncryptResponse(用同一会话密钥 AES-GCM 加密响应回传,请求结束即销毁);②新增 middleware/APICrypto——请求整体解密(io.ReadAll 直接读 r.Request.Body 勿用 GetBody,否则缓存密文致 handler Parse 读到密文)+ 响应加密 buffer(HandlerResponse 外层、Recover 内层,500 也加密),public-key 明文豁免,未加密业务请求一律拒绝;③admin 组中间件链改 CORS→APICrypto→Recover→HandlerResponse;④ResolveLogin/resolvePassword 增 fullBody 分支(传输层已整体解密,直接信任明文 username/password);⑤injectEnv 支持 ENCRYPT_FULL_BODY/ENCRYPT_ALLOW_PLAIN(直接跑二进制恒加载 config.yaml,GF_GCFG_ENV 仅 gf run 认,必须 env 注入)。⚠️GF 坑:r.GetBody() 缓存密文到 bodyContent,handler Parse 走缓存 → 中间件必须 io.ReadAll(r.Request.Body)。dev(仅密码加密)E2E 15 项、prod(全量加密)E2E 9 项全过 2026-08-27 | FEAT | 登录密码「RSA + AES-GCM」混合加密传输(对称+非对称结合)+ 创建/重置密码加密字段:①新建 internal/library/crypto——密钥优先级 配置 encrypt.privateKey(PEM) > data/crypto/rsa_private.pem > 自动生成落盘(data/ 已 gitignore);公钥输出 SPKI(x509.MarshalPKIXPublicKey,前端 WebCrypto importKey('spki'),⚠️PKCS#1 会 ASN.1 wrong tag);DecryptLogin 解密 {username,password,ts}(ts 5 分钟窗口防重放)、DecryptField 解密 {value,ts}(创建/重置密码复用);进程级 SetDefault/Get 单例。②公开接口 POST /system/auth/public-key 返回 publicKey;LoginReq 改 encryptedKey+encryptedData(明文字段仅 encrypt.allowPlain=true 时可用,config.yaml/prod 默认 false、dev true);controller.ResolveLogin 统一解析凭据。③AdminCreate/AdminResetPwd 同样支持加密 password(resolvePassword 复用 DecryptField)。④密文结构:encryptedData=base64(nonce(12B)||ciphertext||tag)、encryptedKey=base64(RSA-OAEP(SHA-256) 加密 AES-256 密钥)。端到端 Node 模拟 WebCrypto 15 项全过(公钥/加密登录/错密码30002/篡改密文/过期载荷/受保护接口/加密创建/加密重置/旧密码失效/登出撤销/登出后刷新拒绝)。注:数据库密码本就是 bcrypt 哈希保存(bcrypt.GenerateFromPassword),存储安全已达标 diff --git a/internal/middleware/encrypt.go b/internal/middleware/encrypt.go index 83f5327..fc908d5 100644 --- a/internal/middleware/encrypt.go +++ b/internal/middleware/encrypt.go @@ -26,8 +26,11 @@ import ( // aesKeyCtxVar 请求上下文中 AES 会话密钥(base64 字符串)的键名。 const aesKeyCtxVar = "__crypto_aes_key" -// plainRoutes 全量加密豁免路由(path 片段匹配),此类端点明文收发。 -var plainRoutes = []string{"/public-key"} +// plainRoutes 全量加密豁免路由(path 片段匹配): +// - /public-key:必须先明文拿到公钥才能发起加密请求; +// - /system/auth/login:登录永远只加密密码字段(username 明文 + 密码字段混合加密), +// 便于客户端兼容与登录联调,登录响应因此保持明文(不含会话密钥)。 +var plainRoutes = []string{"/public-key", "/system/auth/login"} // isPlainRoute 判断当前请求是否属于明文豁免端点。 func isPlainRoute(r *ghttp.Request) bool { diff --git a/internal/service/admin/admin/login/login.go b/internal/service/admin/admin/login/login.go index 308b4ad..55ddc35 100644 --- a/internal/service/admin/admin/login/login.go +++ b/internal/service/admin/admin/login/login.go @@ -70,17 +70,12 @@ func (s *adminAuth) Login(ctx context.Context, in dto.AdminLoginInput) (*dto.Tok return s.issue(ctx, a.Id, "") } -// ResolveLogin 解析登录凭据,返回明文用户名与密码: -// - 全量加密模式(fullBody):传输层 APICrypto 已整体解密,前端提交明文 username/password; -// - 仅密码加密模式:携带密文(encryptedKey + encryptedData)时走 RSA+AES 混合解密, -// 否则仅当服务端 encrypt.allowPlain=true(开发联调)时接受明文,生产一律拒绝。 +// ResolveLogin 解析登录凭据,返回明文用户名与密码。 +// 登录永远只加密密码字段(全量加密模式下同样豁免登录的整体加密): +// - 携带密文(encryptedKey + encryptedData,密码字段 RSA+AES 混合加密)时优先解密; +// - 否则全量加密模式(fullBody):传输层已整体解密,信任前端提交的明文; +// - 开发环境 allowPlain=true 时接受明文,生产一律拒绝。 func (s *adminAuth) ResolveLogin(ctx context.Context, encryptedKey, encryptedData, username, password string) (string, string, error) { - if s.crypto.FullBody() { - if username == "" || password == "" { - return "", "", response.Error(consts.CodeInvalidParam, "username or password required") - } - return username, password, nil - } if encryptedKey != "" && encryptedData != "" { payload, err := s.crypto.DecryptLogin(ctx, encryptedKey, encryptedData) if err != nil { @@ -88,6 +83,12 @@ func (s *adminAuth) ResolveLogin(ctx context.Context, encryptedKey, encryptedDat } return payload.Username, payload.Password, nil } + if s.crypto.FullBody() { + if username == "" || password == "" { + return "", "", response.Error(consts.CodeInvalidParam, "username or password required") + } + return username, password, nil + } if s.crypto.AllowPlain() { return username, password, nil }