chore(auth): 登录接口豁免全量加密,仅加密密码字段
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 37s
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 37s
- APICrypto plainRoutes 增 /system/auth/login(登录请求不整体加密) - ResolveLogin 密码字段密文优先解密,兼容全量模式
This commit is contained in:
parent
ebc507ee6c
commit
70cb86176e
@ -1,6 +1,8 @@
|
|||||||
# service.xpcool.com 变更记录
|
# service.xpcool.com 变更记录
|
||||||
> 倒序:最新在上。格式:YYYY-MM-DD | 类型 | 摘要
|
> 倒序:最新在上。格式: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 | 全量请求/响应加密(生产 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),存储安全已达标
|
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),存储安全已达标
|
||||||
|
|||||||
@ -26,8 +26,11 @@ import (
|
|||||||
// aesKeyCtxVar 请求上下文中 AES 会话密钥(base64 字符串)的键名。
|
// aesKeyCtxVar 请求上下文中 AES 会话密钥(base64 字符串)的键名。
|
||||||
const aesKeyCtxVar = "__crypto_aes_key"
|
const aesKeyCtxVar = "__crypto_aes_key"
|
||||||
|
|
||||||
// plainRoutes 全量加密豁免路由(path 片段匹配),此类端点明文收发。
|
// plainRoutes 全量加密豁免路由(path 片段匹配):
|
||||||
var plainRoutes = []string{"/public-key"}
|
// - /public-key:必须先明文拿到公钥才能发起加密请求;
|
||||||
|
// - /system/auth/login:登录永远只加密密码字段(username 明文 + 密码字段混合加密),
|
||||||
|
// 便于客户端兼容与登录联调,登录响应因此保持明文(不含会话密钥)。
|
||||||
|
var plainRoutes = []string{"/public-key", "/system/auth/login"}
|
||||||
|
|
||||||
// isPlainRoute 判断当前请求是否属于明文豁免端点。
|
// isPlainRoute 判断当前请求是否属于明文豁免端点。
|
||||||
func isPlainRoute(r *ghttp.Request) bool {
|
func isPlainRoute(r *ghttp.Request) bool {
|
||||||
|
|||||||
@ -70,17 +70,12 @@ func (s *adminAuth) Login(ctx context.Context, in dto.AdminLoginInput) (*dto.Tok
|
|||||||
return s.issue(ctx, a.Id, "")
|
return s.issue(ctx, a.Id, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveLogin 解析登录凭据,返回明文用户名与密码:
|
// ResolveLogin 解析登录凭据,返回明文用户名与密码。
|
||||||
// - 全量加密模式(fullBody):传输层 APICrypto 已整体解密,前端提交明文 username/password;
|
// 登录永远只加密密码字段(全量加密模式下同样豁免登录的整体加密):
|
||||||
// - 仅密码加密模式:携带密文(encryptedKey + encryptedData)时走 RSA+AES 混合解密,
|
// - 携带密文(encryptedKey + encryptedData,密码字段 RSA+AES 混合加密)时优先解密;
|
||||||
// 否则仅当服务端 encrypt.allowPlain=true(开发联调)时接受明文,生产一律拒绝。
|
// - 否则全量加密模式(fullBody):传输层已整体解密,信任前端提交的明文;
|
||||||
|
// - 开发环境 allowPlain=true 时接受明文,生产一律拒绝。
|
||||||
func (s *adminAuth) ResolveLogin(ctx context.Context, encryptedKey, encryptedData, username, password string) (string, string, error) {
|
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 != "" {
|
if encryptedKey != "" && encryptedData != "" {
|
||||||
payload, err := s.crypto.DecryptLogin(ctx, encryptedKey, encryptedData)
|
payload, err := s.crypto.DecryptLogin(ctx, encryptedKey, encryptedData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -88,6 +83,12 @@ func (s *adminAuth) ResolveLogin(ctx context.Context, encryptedKey, encryptedDat
|
|||||||
}
|
}
|
||||||
return payload.Username, payload.Password, nil
|
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() {
|
if s.crypto.AllowPlain() {
|
||||||
return username, password, nil
|
return username, password, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user