Files
gaokao-volunteer-system/admin/config.py
Hermes Agent e7706e3b11
Some checks failed
CI / pytest (Python 3.10) (push) Has been cancelled
CI / pytest (Python 3.11) (push) Has been cancelled
CI / pytest (Python 3.12) (push) Has been cancelled
chore: harden real payment onboarding gate
2026-06-14 06:50:21 +08:00

155 lines
6.6 KiB
Python
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.
"""配置加载 (T6.1).
所有运行时配置通过环境变量读取。提供默认值用于开发环境,但启动时
必须输出 WARN 提示生产环境必须显式覆盖。
"""
from __future__ import annotations
import os
import secrets
import string
from dataclasses import dataclass
from fastapi import Request
# 安全占位密钥(仅 dev 环境)。生产必须显式设置 GAOKAO_JWT_SECRET。
_DEV_JWT_SECRET = "dev-only-do-not-use-in-prod-please-override-via-env"
_DEFAULT_ADMIN_PASSWORD = "admin123"
_MIN_ADMIN_PASSWORD_LENGTH = 10
@dataclass(frozen=True)
class Settings:
"""运行时配置(不可变)。"""
env: str
db_path: str
orders_db_path: str # T6.2 — 订单数据 (data.orders.* 写入位置)
share_db_path: str # T7.5 — 短链接 SQLite
share_report_dir: str # T7.5 — report_id -> JSON 报告目录
payment_provider: str # mock|alipay
payment_base_url: str
payment_webhook_secret: str
payment_notify_url: str
payment_return_url: str
payment_app_id: str
payment_private_key_path: str
payment_alipay_public_key_path: str
jwt_secret: str
jwt_algorithm: str
jwt_expire_minutes: int
default_admin_username: str
default_admin_password: str
def load_settings() -> Settings:
"""从环境变量加载配置。
- GAOKAO_ENV : dev|prod默认 dev
- GAOKAO_DB_PATH : 管理后台 SQLite 文件路径,默认 data/orders/admin.db
- GAOKAO_ORDERS_DB_PATH : 订单 DB 路径 (与 data.orders.* 共享),默认 data/orders.db
- GAOKAO_SHARE_DB_PATH : 分享短链接 DB 路径,默认 data/share/short_links.db
- GAOKAO_SHARE_REPORT_DIR : 分享报告 JSON 目录,默认 data/share/reports
- GAOKAO_PAYMENT_PROVIDER : 支付 provider默认 mock
- GAOKAO_PAYMENT_BASE_URL : 支付相关回跳基础 URL默认 http://testserver
- GAOKAO_PAYMENT_WEBHOOK_SECRET : 支付 webhook 独立签名密钥,默认 dev mock secret
- GAOKAO_PAYMENT_NOTIFY_URL : 真实 provider 异步通知地址,默认空
- GAOKAO_PAYMENT_RETURN_URL : 真实 provider 浏览器返回地址,默认空
- GAOKAO_PAYMENT_APP_ID : 真实 provider 应用 ID默认空
- GAOKAO_PAYMENT_PRIVATE_KEY_PATH : 真实 provider 私钥路径,默认空
- GAOKAO_PAYMENT_ALIPAY_PUBLIC_KEY_PATH : 支付宝公钥路径,默认空
- GAOKAO_JWT_SECRET : HS256 密钥,默认 dev 占位(启动日志 WARN
- GAOKAO_JWT_EXP_MIN : JWT 过期时间(分钟),默认 60
- GAOKAO_ADMIN_USER : 默认管理员用户名,默认 admin
- GAOKAO_ADMIN_PASS : 默认管理员密码,默认 admin123仅本地开发占位
Returns:
Settings: 不可变配置实例
"""
return Settings(
env=os.getenv("GAOKAO_ENV", "dev"),
db_path=os.getenv("GAOKAO_DB_PATH", "data/orders/admin.db"),
orders_db_path=os.getenv("GAOKAO_ORDERS_DB_PATH", "data/orders.db"),
share_db_path=os.getenv("GAOKAO_SHARE_DB_PATH", "data/share/short_links.db"),
share_report_dir=os.getenv("GAOKAO_SHARE_REPORT_DIR", "data/share/reports"),
payment_provider=os.getenv("GAOKAO_PAYMENT_PROVIDER", "mock"),
payment_base_url=os.getenv("GAOKAO_PAYMENT_BASE_URL", "http://testserver"),
payment_webhook_secret=os.getenv(
"GAOKAO_PAYMENT_WEBHOOK_SECRET", "dev-mock-payment-secret"
),
payment_notify_url=os.getenv("GAOKAO_PAYMENT_NOTIFY_URL", ""),
payment_return_url=os.getenv("GAOKAO_PAYMENT_RETURN_URL", ""),
payment_app_id=os.getenv("GAOKAO_PAYMENT_APP_ID", ""),
payment_private_key_path=os.getenv("GAOKAO_PAYMENT_PRIVATE_KEY_PATH", ""),
payment_alipay_public_key_path=os.getenv(
"GAOKAO_PAYMENT_ALIPAY_PUBLIC_KEY_PATH", ""
),
jwt_secret=os.getenv("GAOKAO_JWT_SECRET", _DEV_JWT_SECRET),
jwt_algorithm=os.getenv("GAOKAO_JWT_ALGORITHM", "HS256"),
jwt_expire_minutes=int(os.getenv("GAOKAO_JWT_EXP_MIN", "60")),
default_admin_username=os.getenv("GAOKAO_ADMIN_USER", "admin"),
default_admin_password=os.getenv("GAOKAO_ADMIN_PASS", _DEFAULT_ADMIN_PASSWORD),
)
def is_jwt_secret_secure(settings: Settings) -> tuple:
"""判断 JWT 密钥是否满足最低安全门槛。
Returns:
(is_secure, reason) : 不满足时给出原因字符串
"""
secret = settings.jwt_secret
if settings.env == "prod":
if secret == _DEV_JWT_SECRET:
return False, "生产环境禁止使用 dev 占位密钥"
if len(secret) < 32:
return False, "生产环境 JWT 密钥长度必须 >= 32 (当前 {})".format(
len(secret)
)
if secret == _DEV_JWT_SECRET and settings.env == "dev":
return False, "dev 环境使用占位密钥(仅本地开发可接受)"
if len(secret) < 32:
return False, "JWT 密钥长度必须 >= 32 (当前 {})".format(len(secret))
return True, "ok"
def is_default_admin_password_secure(settings: Settings) -> tuple[bool, str]:
"""判断默认管理员密码是否满足最低安全门槛。"""
password = settings.default_admin_password
if len(password) < _MIN_ADMIN_PASSWORD_LENGTH:
return False, (
f"默认管理员密码长度必须 >= {_MIN_ADMIN_PASSWORD_LENGTH} (当前 {len(password)})"
)
if settings.env == "prod" and password == _DEFAULT_ADMIN_PASSWORD:
return False, "生产环境禁止使用默认管理员密码 admin123"
if settings.env == "prod":
classes = sum((
any(ch.islower() for ch in password),
any(ch.isupper() for ch in password),
any(ch.isdigit() for ch in password),
any(ch in string.punctuation for ch in password),
))
if classes < 3:
return False, "生产环境默认管理员密码至少覆盖 3 类字符(大小写/数字/符号)"
if settings.env == "dev" and password == _DEFAULT_ADMIN_PASSWORD:
return False, "dev 环境仍在使用默认管理员密码(仅本地临时开发可接受)"
return True, "ok"
def generate_secure_secret() -> str:
"""生成 32-byte 十六进制密钥(用于初始化文档示例)。"""
return secrets.token_hex(32)
def get_settings_dep(request: Request) -> Settings:
"""FastAPI 依赖:从 app.state 取已加载的 Settings。
与 admin.auth.get_settings 重复,但放在 config 里便于其他路由直接引用。
"""
settings = getattr(request.app.state, "settings", None)
if settings is None: # pragma: no cover - 兜底
settings = load_settings()
return settings