Files
hms/crates/erp-server/src/config.rs
iven 0f67f1c21f fix(server): 限流中间件 fail-close 安全加固
RateLimitConfig 添加 fail_close 字段(默认 true),Redis 不可达时
拒绝请求返回 503 而非静默放行。开发环境可通过
ERP__RATE_LIMIT__FAIL_CLOSE=false 回退旧行为。
2026-05-11 10:22:05 +08:00

199 lines
5.2 KiB
Rust
Raw 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.
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct AppConfig {
pub server: ServerConfig,
pub database: DatabaseConfig,
pub redis: RedisConfig,
pub jwt: JwtConfig,
pub auth: AuthConfig,
pub log: LogConfig,
pub cors: CorsConfig,
pub wechat: WechatConfig,
pub health: HealthConfig,
pub crypto: CryptoConfig,
pub ai: AiConfig,
pub storage: StorageConfig,
#[serde(default)]
pub rate_limit: RateLimitConfig,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
#[serde(default = "default_metrics_port")]
pub metrics_port: u16,
}
fn default_metrics_port() -> u16 {
9090
}
#[derive(Debug, Clone, Deserialize)]
pub struct DatabaseConfig {
pub url: String,
pub max_connections: u32,
pub min_connections: u32,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RedisConfig {
pub url: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct JwtConfig {
pub secret: String,
pub access_token_ttl: String,
pub refresh_token_ttl: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LogConfig {
pub level: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AuthConfig {
pub super_admin_password: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CorsConfig {
/// Comma-separated list of allowed origins.
/// Use "*" to allow all origins (development only).
pub allowed_origins: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WechatConfig {
pub appid: String,
pub secret: String,
#[serde(default)]
pub dev_mode: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HealthConfig {
/// AES-256 密钥 (64 字符 hex 编码32 字节)
pub aes_key: String,
/// HMAC-SHA256 密钥 (64 字符 hex 编码32 字节)
pub hmac_key: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CryptoConfig {
/// Master KEK (64 字符 hex 编码32 字节)。用于加密保护每租户 DEK。
/// Phase A 阶段同时作为全局数据加密密钥使用。
pub kek: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AiConfig {
pub default_provider: String,
pub api_key: String,
pub base_url: Option<String>,
pub model: String,
pub max_tokens: u32,
pub temperature: f32,
pub cache_ttl_seconds: u64,
pub rate_limit_patient_daily: u32,
#[serde(default)]
pub quota_check_enabled: bool,
#[serde(default)]
pub providers: std::collections::HashMap<String, ProviderConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ProviderConfig {
pub provider_type: String,
pub api_key_env: Option<String>,
pub base_url: Option<String>,
pub default_model: String,
pub max_tokens: u32,
pub temperature: f32,
#[serde(default = "default_true")]
pub is_enabled: bool,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Deserialize)]
pub struct StorageConfig {
/// 文件上传目录(本地存储)
pub upload_dir: String,
/// 单文件最大大小(如 "10MB"
pub max_file_size: String,
/// 签名 URL 密钥HMAC-SHA256
#[serde(default = "default_secret_key")]
pub secret_key: String,
}
fn default_secret_key() -> String {
#[cfg(debug_assertions)]
{
"dev-only-secret-key-change-in-production".to_string()
}
#[cfg(not(debug_assertions))]
{
panic!("ERP__STORAGE__SECRET_KEY 必须设置(生产环境不允许使用默认签名密钥)")
}
}
impl StorageConfig {
/// 解析 max_file_size 为字节数
pub fn max_file_size_bytes(&self) -> u64 {
let s = self.max_file_size.to_uppercase();
if let Some(num) = s.strip_suffix("MB") {
num.trim().parse::<u64>().unwrap_or(10) * 1024 * 1024
} else if let Some(num) = s.strip_suffix("KB") {
num.trim().parse::<u64>().unwrap_or(1024) * 1024
} else if let Some(num) = s.strip_suffix("GB") {
num.trim().parse::<u64>().unwrap_or(1) * 1024 * 1024 * 1024
} else {
s.parse::<u64>().unwrap_or(10 * 1024 * 1024)
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct RateLimitConfig {
/// Redis 不可达时是否拒绝请求fail-close
/// true = 安全优先Redis 故障时返回 503。
/// false = 可用性优先Redis 故障时放行。
#[serde(default = "default_fail_close")]
pub fail_close: bool,
}
fn default_fail_close() -> bool {
true
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
fail_close: default_fail_close(),
}
}
}
impl AppConfig {
pub fn load() -> anyhow::Result<Self> {
let config = config::Config::builder()
.add_source(config::File::with_name("config/default"))
.add_source(config::Environment::with_prefix("ERP").separator("__"))
.build()?;
let app_config: Self = config.try_deserialize()?;
// 安全检查:禁止在生产使用默认 JWT 密钥
if app_config.jwt.secret == "change-me-in-production" {
tracing::warn!("⚠️ JWT 密钥使用默认值,请通过 ERP__JWT__SECRET 环境变量设置安全密钥");
}
Ok(app_config)
}
}