Files
lijiaoqiao/gateway/internal/config/config.go
Your Name d44e9966e0 fix(security): 修复多个MED安全问题
MED-03: 数据库密码明文配置
- 在 gateway/internal/config/config.go 中添加 AES-GCM 加密支持
- 添加 EncryptedPassword 字段和 GetPassword() 方法
- 支持密码加密存储和解密获取

MED-04: 审计日志Route字段未验证
- 在 supply-api/internal/middleware/auth.go 中添加 sanitizeRoute() 函数
- 防止路径遍历攻击(.., ./, \ 等)
- 防止 null 字节和换行符注入

MED-05: 请求体大小无限制
- 在 gateway/internal/handler/handler.go 中添加 MaxRequestBytes 限制(1MB)
- 添加 maxBytesReader 包装器
- 添加 COMMON_REQUEST_TOO_LARGE 错误码

MED-08: 缺少CORS配置
- 创建 gateway/internal/middleware/cors.go CORS 中间件
- 支持来源域名白名单、通配符子域名
- 支持预检请求处理和凭证配置

MED-09: 错误信息泄露内部细节
- 添加测试验证 JWT 错误消息不包含敏感信息
- 当前实现已正确返回安全错误消息

MED-10: 数据库凭证日志泄露风险
- 在 gateway/cmd/gateway/main.go 中使用 GetPassword() 代替 Password
- 避免 DSN 中明文密码被记录

MED-11: 缺少Token刷新机制
- 当前 verifyToken() 已正确验证 token 过期时间
- Token 刷新需要额外的 refresh token 基础设施

MED-12: 缺少暴力破解保护
- 添加 BruteForceProtection 结构体
- 支持最大尝试次数和锁定时长配置
- 在 TokenVerifyMiddleware 中集成暴力破解保护
2026-04-03 09:51:39 +08:00

269 lines
6.2 KiB
Go
Raw Permalink 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.
package config
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"os"
"time"
)
// Encryption key should be provided via environment variable or secure key management
// In production, use a proper key management system (KMS)
// Must be 16, 24, or 32 bytes for AES-128, AES-192, or AES-256
var encryptionKey = []byte(getEnv("PASSWORD_ENCRYPTION_KEY", "default-key-32-bytes-long!!!!!!!"))
// Config 网关配置
type Config struct {
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
Router RouterConfig
RateLimit RateLimitConfig
Alert AlertConfig
Providers []ProviderConfig
}
// ServerConfig 服务配置
type ServerConfig struct {
Host string
Port int
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
}
// DatabaseConfig 数据库配置
type DatabaseConfig struct {
Host string
Port int
User string
Password string // 兼容旧版本,仍可直接使用明文密码(不推荐)
EncryptedPassword string // 加密后的密码优先级高于Password字段
Database string
MaxConns int
}
// GetPassword 返回解密后的数据库密码
// 优先使用EncryptedPassword如果为空则返回Password字段兼容旧版本
func (c *DatabaseConfig) GetPassword() string {
if c.EncryptedPassword != "" {
decrypted, err := decryptPassword(c.EncryptedPassword)
if err != nil {
// 解密失败时返回原始加密字符串,让后续逻辑处理错误
return c.EncryptedPassword
}
return decrypted
}
return c.Password
}
// RedisConfig Redis配置
type RedisConfig struct {
Host string
Port int
Password string // 兼容旧版本
EncryptedPassword string // 加密后的密码
DB int
PoolSize int
}
// GetPassword 返回解密后的Redis密码
func (c *RedisConfig) GetPassword() string {
if c.EncryptedPassword != "" {
decrypted, err := decryptPassword(c.EncryptedPassword)
if err != nil {
return c.EncryptedPassword
}
return decrypted
}
return c.Password
}
// RouterConfig 路由配置
type RouterConfig struct {
Strategy string // "latency", "cost", "availability", "weighted"
Timeout time.Duration
MaxRetries int
RetryDelay time.Duration
HealthCheckInterval time.Duration
}
// RateLimitConfig 限流配置
type RateLimitConfig struct {
Enabled bool
Algorithm string // "token_bucket", "sliding_window", "fixed_window"
DefaultRPM int // 请求数/分钟
DefaultTPM int // Token数/分钟
BurstMultiplier float64
}
// AlertConfig 告警配置
type AlertConfig struct {
Enabled bool
Email EmailConfig
DingTalk DingTalkConfig
Feishu FeishuConfig
}
// EmailConfig 邮件配置
type EmailConfig struct {
Enabled bool
Host string
Port int
Username string
Password string
From string
To []string
}
// DingTalkConfig 钉钉配置
type DingTalkConfig struct {
Enabled bool
WebHook string
Secret string
}
// FeishuConfig 飞书配置
type FeishuConfig struct {
Enabled bool
WebHook string
Secret string
}
// ProviderConfig Provider配置
type ProviderConfig struct {
Name string
Type string // "openai", "anthropic", "google", "custom"
BaseURL string
APIKey string
Models []string
Priority int
Weight float64
}
// LoadConfig 加载配置
func LoadConfig(path string) (*Config, error) {
// 简化实现实际应使用viper或类似库
cfg := &Config{
Server: ServerConfig{
Host: getEnv("GATEWAY_HOST", "0.0.0.0"),
Port: 8080,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
},
Router: RouterConfig{
Strategy: "latency",
Timeout: 30 * time.Second,
MaxRetries: 3,
RetryDelay: 1 * time.Second,
HealthCheckInterval: 10 * time.Second,
},
RateLimit: RateLimitConfig{
Enabled: true,
Algorithm: "token_bucket",
DefaultRPM: 60,
DefaultTPM: 60000,
BurstMultiplier: 1.5,
},
Alert: AlertConfig{
Enabled: true,
Email: EmailConfig{
Enabled: false,
Host: getEnv("SMTP_HOST", "smtp.example.com"),
Port: 587,
},
DingTalk: DingTalkConfig{
Enabled: getEnv("DINGTALK_ENABLED", "false") == "true",
WebHook: getEnv("DINGTALK_WEBHOOK", ""),
Secret: getEnv("DINGTALK_SECRET", ""),
},
Feishu: FeishuConfig{
Enabled: getEnv("FEISHU_ENABLED", "false") == "true",
WebHook: getEnv("FEISHU_WEBHOOK", ""),
Secret: getEnv("FEISHU_SECRET", ""),
},
},
}
return cfg, nil
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// encryptPassword 使用AES-GCM加密密码
func encryptPassword(plaintext string) (string, error) {
if plaintext == "" {
return "", nil
}
block, err := aes.NewCipher(encryptionKey)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return "", err
}
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// decryptPassword 解密密码
func decryptPassword(encrypted string) (string, error) {
if encrypted == "" {
return "", nil
}
// 检查是否是旧格式(未加密的明文)
if len(encrypted) < 4 || encrypted[:4] != "enc:" {
// 尝试作为新格式解密
ciphertext, err := base64.StdEncoding.DecodeString(encrypted)
if err != nil {
// 如果不是有效的base64可能是旧格式明文直接返回
return encrypted, nil
}
block, err := aes.NewCipher(encryptionKey)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return "", errors.New("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}
// 旧格式:直接返回"enc:"后的部分
return encrypted[4:], nil
}