diff --git a/api/server.go b/api/server.go index 01c2c0ae..e98db44a 100644 --- a/api/server.go +++ b/api/server.go @@ -1077,7 +1077,7 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) { // 这里不返回错误,因为模型配置已经成功更新到数据库 } - log.Printf("✓ AI模型配置已更新: %+v", req.Models) + log.Printf("✓ AI模型配置已更新: %+v", SanitizeModelConfigForLog(req.Models)) c.JSON(http.StatusOK, gin.H{"message": "模型配置已更新"}) } @@ -1174,7 +1174,7 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) { // 这里不返回错误,因为交易所配置已经成功更新到数据库 } - log.Printf("✓ 交易所配置已更新: %+v", req.Exchanges) + log.Printf("✓ 交易所配置已更新: %+v", SanitizeExchangeConfigForLog(req.Exchanges)) c.JSON(http.StatusOK, gin.H{"message": "交易所配置已更新"}) } diff --git a/api/utils.go b/api/utils.go new file mode 100644 index 00000000..4f871ef0 --- /dev/null +++ b/api/utils.go @@ -0,0 +1,97 @@ +package api + +import "strings" + +// MaskSensitiveString 脱敏敏感字符串,只显示前4位和后4位 +// 用于脱敏 API Key、Secret Key、Private Key 等敏感信息 +func MaskSensitiveString(s string) string { + if s == "" { + return "" + } + length := len(s) + if length <= 8 { + return "****" // 字符串太短,全部隐藏 + } + return s[:4] + "****" + s[length-4:] +} + +// SanitizeModelConfigForLog 脱敏模型配置用于日志输出 +func SanitizeModelConfigForLog(models map[string]struct { + Enabled bool `json:"enabled"` + APIKey string `json:"api_key"` + CustomAPIURL string `json:"custom_api_url"` + CustomModelName string `json:"custom_model_name"` +}) map[string]interface{} { + safe := make(map[string]interface{}) + for modelID, cfg := range models { + safe[modelID] = map[string]interface{}{ + "enabled": cfg.Enabled, + "api_key": MaskSensitiveString(cfg.APIKey), + "custom_api_url": cfg.CustomAPIURL, + "custom_model_name": cfg.CustomModelName, + } + } + return safe +} + +// SanitizeExchangeConfigForLog 脱敏交易所配置用于日志输出 +func SanitizeExchangeConfigForLog(exchanges map[string]struct { + Enabled bool `json:"enabled"` + APIKey string `json:"api_key"` + SecretKey string `json:"secret_key"` + Testnet bool `json:"testnet"` + HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"` + AsterUser string `json:"aster_user"` + AsterSigner string `json:"aster_signer"` + AsterPrivateKey string `json:"aster_private_key"` +}) map[string]interface{} { + safe := make(map[string]interface{}) + for exchangeID, cfg := range exchanges { + safeExchange := map[string]interface{}{ + "enabled": cfg.Enabled, + "testnet": cfg.Testnet, + } + + // 只在有值时才添加脱敏后的敏感字段 + if cfg.APIKey != "" { + safeExchange["api_key"] = MaskSensitiveString(cfg.APIKey) + } + if cfg.SecretKey != "" { + safeExchange["secret_key"] = MaskSensitiveString(cfg.SecretKey) + } + if cfg.AsterPrivateKey != "" { + safeExchange["aster_private_key"] = MaskSensitiveString(cfg.AsterPrivateKey) + } + + // 非敏感字段直接添加 + if cfg.HyperliquidWalletAddr != "" { + safeExchange["hyperliquid_wallet_addr"] = cfg.HyperliquidWalletAddr + } + if cfg.AsterUser != "" { + safeExchange["aster_user"] = cfg.AsterUser + } + if cfg.AsterSigner != "" { + safeExchange["aster_signer"] = cfg.AsterSigner + } + + safe[exchangeID] = safeExchange + } + return safe +} + +// MaskEmail 脱敏邮箱地址,保留前2位和@后部分 +func MaskEmail(email string) string { + if email == "" { + return "" + } + parts := strings.Split(email, "@") + if len(parts) != 2 { + return "****" // 格式不正确 + } + username := parts[0] + domain := parts[1] + if len(username) <= 2 { + return "**@" + domain + } + return username[:2] + "****@" + domain +} diff --git a/api/utils_test.go b/api/utils_test.go new file mode 100644 index 00000000..fb4976ff --- /dev/null +++ b/api/utils_test.go @@ -0,0 +1,193 @@ +package api + +import ( + "testing" +) + +func TestMaskSensitiveString(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "空字符串", + input: "", + expected: "", + }, + { + name: "短字符串(小于等于8位)", + input: "short", + expected: "****", + }, + { + name: "正常API key", + input: "sk-1234567890abcdefghijklmnopqrstuvwxyz", + expected: "sk-1****wxyz", + }, + { + name: "正常私钥", + input: "0x1234567890abcdef1234567890abcdef12345678", + expected: "0x12****5678", + }, + { + name: "刚好9位", + input: "123456789", + expected: "1234****6789", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := MaskSensitiveString(tt.input) + if result != tt.expected { + t.Errorf("MaskSensitiveString(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestSanitizeModelConfigForLog(t *testing.T) { + models := map[string]struct { + Enabled bool `json:"enabled"` + APIKey string `json:"api_key"` + CustomAPIURL string `json:"custom_api_url"` + CustomModelName string `json:"custom_model_name"` + }{ + "deepseek": { + Enabled: true, + APIKey: "sk-1234567890abcdefghijklmnopqrstuvwxyz", + CustomAPIURL: "https://api.deepseek.com", + CustomModelName: "deepseek-chat", + }, + } + + result := SanitizeModelConfigForLog(models) + + deepseekConfig, ok := result["deepseek"].(map[string]interface{}) + if !ok { + t.Fatal("deepseek config not found or wrong type") + } + + if deepseekConfig["enabled"] != true { + t.Errorf("expected enabled=true, got %v", deepseekConfig["enabled"]) + } + + maskedKey, ok := deepseekConfig["api_key"].(string) + if !ok { + t.Fatal("api_key not found or wrong type") + } + + if maskedKey != "sk-1****wxyz" { + t.Errorf("expected masked api_key='sk-1****wxyz', got %q", maskedKey) + } + + if deepseekConfig["custom_api_url"] != "https://api.deepseek.com" { + t.Errorf("custom_api_url should not be masked") + } +} + +func TestSanitizeExchangeConfigForLog(t *testing.T) { + exchanges := map[string]struct { + Enabled bool `json:"enabled"` + APIKey string `json:"api_key"` + SecretKey string `json:"secret_key"` + Testnet bool `json:"testnet"` + HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"` + AsterUser string `json:"aster_user"` + AsterSigner string `json:"aster_signer"` + AsterPrivateKey string `json:"aster_private_key"` + }{ + "binance": { + Enabled: true, + APIKey: "binance_api_key_1234567890abcdef", + SecretKey: "binance_secret_key_1234567890abcdef", + Testnet: false, + }, + "hyperliquid": { + Enabled: true, + HyperliquidWalletAddr: "0x1234567890abcdef1234567890abcdef12345678", + Testnet: false, + }, + } + + result := SanitizeExchangeConfigForLog(exchanges) + + // 检查币安配置 + binanceConfig, ok := result["binance"].(map[string]interface{}) + if !ok { + t.Fatal("binance config not found or wrong type") + } + + maskedAPIKey, ok := binanceConfig["api_key"].(string) + if !ok { + t.Fatal("binance api_key not found or wrong type") + } + + if maskedAPIKey != "bina****cdef" { + t.Errorf("expected masked api_key='bina****cdef', got %q", maskedAPIKey) + } + + maskedSecretKey, ok := binanceConfig["secret_key"].(string) + if !ok { + t.Fatal("binance secret_key not found or wrong type") + } + + if maskedSecretKey != "bina****cdef" { + t.Errorf("expected masked secret_key='bina****cdef', got %q", maskedSecretKey) + } + + // 检查 Hyperliquid 配置 + hlConfig, ok := result["hyperliquid"].(map[string]interface{}) + if !ok { + t.Fatal("hyperliquid config not found or wrong type") + } + + walletAddr, ok := hlConfig["hyperliquid_wallet_addr"].(string) + if !ok { + t.Fatal("hyperliquid_wallet_addr not found or wrong type") + } + + // 钱包地址不应该被脱敏 + if walletAddr != "0x1234567890abcdef1234567890abcdef12345678" { + t.Errorf("wallet address should not be masked, got %q", walletAddr) + } +} + +func TestMaskEmail(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "空邮箱", + input: "", + expected: "", + }, + { + name: "格式错误", + input: "notanemail", + expected: "****", + }, + { + name: "正常邮箱", + input: "user@example.com", + expected: "us****@example.com", + }, + { + name: "短用户名", + input: "a@example.com", + expected: "**@example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := MaskEmail(tt.input) + if result != tt.expected { + t.Errorf("MaskEmail(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +}