import os import logging import secrets as _secrets from pydantic_settings import BaseSettings from pydantic import field_validator from functools import lru_cache logger = logging.getLogger(__name__) # 已知的弱/默认密钥,实例化时会拒绝(防止源码泄露后被伪造 JWT) _INSECURE_DEFAULTS = { "", "change-me-in-production", "changeme", "secret", "your-secret-key", } class Settings(BaseSettings): APP_NAME: str = "智教助手" APP_VERSION: str = "1.0.0" DEBUG: bool = True DATABASE_URL: str = "sqlite:///./jiaoyu.db" REDIS_URL: str = "redis://localhost:6379/0" SECRET_KEY: str = "change-me-in-production" ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 REFRESH_TOKEN_EXPIRE_DAYS: int = 7 AI_API_KEY: str = "" AI_API_BASE: str = "https://api.openai.com/v1" AI_MODEL: str = "gpt-4o" CORS_ORIGINS: list[str] = ["http://localhost:5173"] UPLOAD_DIR: str = "uploads" MAX_UPLOAD_SIZE: int = 50 * 1024 * 1024 MAX_REQUEST_BODY_SIZE: int = 60 * 1024 * 1024 # global request body limit @field_validator("SECRET_KEY") @classmethod def _validate_secret_key(cls, v: str) -> str: """在 .env 加载后执行:弱默认值在开发模式生成临时密钥,生产模式拒绝启动。""" raw = (v or "").strip() debug_env = str(os.getenv("DEBUG", "")).lower() # DEBUG 字段可能已被 pydantic 解析;优先看环境变量原始值,回退看已解析的 DEBUG is_debug = debug_env in ("", "1", "true", "yes", "on") if debug_env else True # 若环境变量显式设了 DEBUG,用它;否则用 pydantic 解析后的值(默认 True) if debug_env: is_debug = debug_env in ("1", "true", "yes", "on") if raw in _INSECURE_DEFAULTS: if is_debug: generated = _secrets.token_urlsafe(48) logger.warning( "SECRET_KEY 未配置或为弱默认值,已生成临时开发密钥(仅本次运行有效)。" "生产环境请在 .env 中设置 SECRET_KEY 为至少 32 字符的随机字符串。" ) return generated raise RuntimeError( "SECRET_KEY 未配置或为弱默认值,生产环境拒绝启动。" "请在 .env 中设置 SECRET_KEY 为至少 32 字符的随机字符串。" ) if len(raw) < 32: if is_debug: logger.warning( "SECRET_KEY 长度不足 32 字符(当前 %d),建议使用更长的随机字符串。", len(raw) ) return raw raise RuntimeError( f"SECRET_KEY 长度不足 32 字符(当前 {len(raw)}),生产环境拒绝启动。" ) return raw class Config: env_file = ".env" extra = "ignore" @lru_cache() def get_settings() -> Settings: return Settings()