package config import ( "errors" "os" "strconv" "strings" ) type Config struct { AppEnv string AppAddr string MySQLDSN string RedisAddr string RedisPassword string RedisDB int JWTSecret string PaymentConfigEncryptionKey string ExternalUploadSecret string ExternalUploadAllowedIPs []string BootstrapAdminUsername string BootstrapAdminPassword string BootstrapAdminNickname string Storage StorageConfig SMS SMSConfig Realname RealnameConfig Log LogConfig RateLimit RateLimitConfig } type StorageConfig struct { Endpoint string Bucket string AccessKeyID string SecretAccessKey string } type SMSConfig struct { Provider string AliyunAccessKeyID string AliyunAccessKeySecret string AliyunEndpoint string AliyunSignName string AliyunLoginTemplateCode string } type RealnameConfig struct { Provider string CloudMarketURL string CloudMarketAppCode string } type LogConfig struct { Level string Dir string EnableConsole bool EnableFile bool } type RateLimitConfig struct { Enabled bool RequestsPerMinute int } func Load() Config { return Config{ AppEnv: getEnv("APP_ENV", "development"), AppAddr: getEnv("APP_ADDR", ":8080"), MySQLDSN: getEnv("MYSQL_DSN", "hfb:secret@tcp(127.0.0.1:3306)/hfb_sys?charset=utf8mb4&parseTime=True&loc=Local"), RedisAddr: getEnv("REDIS_ADDR", "127.0.0.1:6379"), RedisPassword: getEnv("REDIS_PASSWORD", ""), RedisDB: getEnvInt("REDIS_DB", 0), JWTSecret: getEnv("JWT_SECRET", "change-me"), PaymentConfigEncryptionKey: getEnv("PAYMENT_CONFIG_ENCRYPTION_KEY", ""), ExternalUploadSecret: getEnv("EXTERNAL_UPLOAD_SECRET", ""), ExternalUploadAllowedIPs: getEnvList("EXTERNAL_UPLOAD_ALLOWED_IPS"), BootstrapAdminUsername: getEnv("ADMIN_BOOTSTRAP_USERNAME", ""), BootstrapAdminPassword: getEnv("ADMIN_BOOTSTRAP_PASSWORD", ""), BootstrapAdminNickname: getEnv("ADMIN_BOOTSTRAP_NICKNAME", "超级管理员"), Storage: StorageConfig{ Endpoint: getEnv("STORAGE_ENDPOINT", "http://localhost:9000"), Bucket: getEnv("STORAGE_BUCKET", "hfb-sys"), AccessKeyID: getEnv("STORAGE_ACCESS_KEY_ID", "minioadmin"), SecretAccessKey: getEnv("STORAGE_SECRET_ACCESS_KEY", "minioadmin"), }, SMS: SMSConfig{ Provider: getEnv("SMS_PROVIDER", "mock"), AliyunAccessKeyID: getEnv("ALIYUN_ACCESS_KEY_ID", ""), AliyunAccessKeySecret: getEnv("ALIYUN_ACCESS_KEY_SECRET", ""), AliyunEndpoint: getEnv("ALIYUN_SMS_ENDPOINT", "dysmsapi.aliyuncs.com"), AliyunSignName: getEnv("ALIYUN_SMS_SIGN_NAME", ""), AliyunLoginTemplateCode: getEnv("ALIYUN_SMS_LOGIN_TEMPLATE_CODE", ""), }, Realname: RealnameConfig{ Provider: getEnv("REALNAME_PROVIDER", "mock"), CloudMarketURL: getEnv("REALNAME_CLOUDMARKET_URL", "https://sinocheck2.market.alicloudapi.com/fortest/ttttt"), CloudMarketAppCode: getEnv("REALNAME_CLOUDMARKET_APPCODE", ""), }, Log: LogConfig{ Level: getEnv("LOG_LEVEL", "info"), Dir: getEnv("LOG_DIR", "logs"), EnableConsole: getEnvBool("LOG_ENABLE_CONSOLE", true), EnableFile: getEnvBool("LOG_ENABLE_FILE", true), }, RateLimit: RateLimitConfig{ Enabled: getEnvBool("RATE_LIMIT_ENABLED", true), RequestsPerMinute: getEnvInt("RATE_LIMIT_REQUESTS_PER_MINUTE", 300), }, } } func (c Config) ValidateProductionSecurity() error { if strings.ToLower(strings.TrimSpace(c.AppEnv)) != "production" { return nil } if strings.TrimSpace(c.JWTSecret) == "" || isPlaceholder(c.JWTSecret) || len([]byte(c.JWTSecret)) < 32 { return errors.New("JWT_SECRET must be a non-default random value of at least 32 bytes in production") } keyLen := len([]byte(c.PaymentConfigEncryptionKey)) if isPlaceholder(c.PaymentConfigEncryptionKey) || (keyLen != 16 && keyLen != 24 && keyLen != 32) { return errors.New("PAYMENT_CONFIG_ENCRYPTION_KEY must be 16, 24, or 32 bytes in production") } if c.BootstrapAdminPassword != "" && isPlaceholder(c.BootstrapAdminPassword) { return errors.New("ADMIN_BOOTSTRAP_PASSWORD must not use the example placeholder in production") } return nil } func getEnv(key, fallback string) string { value := os.Getenv(key) if value == "" { return fallback } return value } func getEnvInt(key string, fallback int) int { value := os.Getenv(key) if value == "" { return fallback } parsed, err := strconv.Atoi(value) if err != nil { return fallback } return parsed } func getEnvBool(key string, fallback bool) bool { value := os.Getenv(key) if value == "" { return fallback } parsed, err := strconv.ParseBool(value) if err != nil { return fallback } return parsed } func getEnvList(key string) []string { raw := os.Getenv(key) if raw == "" { return nil } parts := strings.Split(raw, ",") values := make([]string, 0, len(parts)) for _, part := range parts { value := strings.TrimSpace(part) if value != "" { values = append(values, value) } } return values } func isPlaceholder(value string) bool { normalized := strings.ToLower(strings.TrimSpace(value)) return normalized == "change-me" || strings.HasPrefix(normalized, "change-") || strings.Contains(normalized, "change-to-") }