feat: 加密并隐藏系统敏感配置
This commit is contained in:
@@ -13,6 +13,7 @@ from app.core.config import get_settings
|
||||
from app.models.ai_config import SystemConfig
|
||||
from app.services.external_errors import ExternalServiceError
|
||||
from app.services.knowledge_service import KnowledgeScope
|
||||
from app.services.secret_service import SecretService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.services.rag_service import RetrievedChunk
|
||||
@@ -812,7 +813,7 @@ def _feishu_retrieval_config(db: Session | None) -> FeishuRetrievalConfig:
|
||||
return FeishuRetrievalConfig(
|
||||
search_url=_config_text(db, "feishu_search_url", settings.feishu_search_url),
|
||||
app_id=_config_text(db, "feishu_app_id", settings.feishu_app_id),
|
||||
app_secret=_config_text(db, "feishu_app_secret", settings.feishu_app_secret),
|
||||
app_secret=SecretService.decrypt(_config_text(db, "feishu_app_secret", settings.feishu_app_secret)),
|
||||
timeout_seconds=_config_int(db, "feishu_timeout_seconds", settings.feishu_timeout_seconds, minimum=1, maximum=120),
|
||||
retry_count=_config_int(db, "feishu_retry_count", settings.feishu_retry_count, minimum=0, maximum=10),
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ from app.core.config import get_settings
|
||||
from app.models.ai_config import ModelConfig, SystemConfig
|
||||
from app.services.external_errors import ExternalServiceError
|
||||
from app.services.rag_service import NO_HIT_ANSWER, RagResult
|
||||
from app.services.secret_service import SecretService
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -343,7 +344,7 @@ def _call_minimax(model: ModelConfig, rag_result: RagResult) -> str:
|
||||
_put_if_not_none(payload, "temperature", _decimal_to_float(model.temperature))
|
||||
_put_if_not_none(payload, "top_p", _decimal_to_float(model.top_p))
|
||||
|
||||
headers = {"Authorization": f"Bearer {model.api_key}", "Content-Type": "application/json"}
|
||||
headers = {"Authorization": f"Bearer {_model_api_key(model)}", "Content-Type": "application/json"}
|
||||
|
||||
try:
|
||||
response = httpx.post(base_url, json=payload, headers=headers, timeout=model.timeout_second)
|
||||
@@ -398,16 +399,16 @@ def _resolve_gemini_endpoint(model: ModelConfig) -> str:
|
||||
if model.api_url:
|
||||
return model.api_url
|
||||
base_url = (model.base_url or "https://generativelanguage.googleapis.com/v1beta").rstrip("/")
|
||||
query = urlencode({"key": model.api_key})
|
||||
query = urlencode({"key": _model_api_key(model)})
|
||||
return f"{base_url}/models/{model.model_name}:generateContent?{query}"
|
||||
|
||||
|
||||
def _auth_headers(model: ModelConfig) -> dict[str, str]:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if model.auth_type == "api_key":
|
||||
headers["x-api-key"] = model.api_key
|
||||
headers["x-api-key"] = _model_api_key(model)
|
||||
else:
|
||||
headers["Authorization"] = f"Bearer {model.api_key}"
|
||||
headers["Authorization"] = f"Bearer {_model_api_key(model)}"
|
||||
if model.api_version:
|
||||
headers["api-version"] = model.api_version
|
||||
return headers
|
||||
@@ -416,9 +417,9 @@ def _auth_headers(model: ModelConfig) -> dict[str, str]:
|
||||
def _anthropic_headers(model: ModelConfig) -> dict[str, str]:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if model.auth_type == "bearer":
|
||||
headers["Authorization"] = f"Bearer {model.api_key}"
|
||||
headers["Authorization"] = f"Bearer {_model_api_key(model)}"
|
||||
else:
|
||||
headers["x-api-key"] = model.api_key
|
||||
headers["x-api-key"] = _model_api_key(model)
|
||||
if model.api_version:
|
||||
headers["anthropic-version"] = model.api_version
|
||||
elif model.provider == "anthropic":
|
||||
@@ -430,6 +431,10 @@ def _decimal_to_float(value: Any) -> float | None:
|
||||
return float(value) if value is not None else None
|
||||
|
||||
|
||||
def _model_api_key(model: ModelConfig) -> str:
|
||||
return SecretService.decrypt(model.api_key)
|
||||
|
||||
|
||||
def _put_if_not_none(target: dict[str, Any], key: str, value: Any) -> None:
|
||||
if value is not None:
|
||||
target[key] = value
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
|
||||
ENCRYPTED_PREFIX = "enc:v1:"
|
||||
MASKED_SECRET = "******"
|
||||
SENSITIVE_CONFIG_KEYS = {"aliyun_sms_access_key_secret", "feishu_app_secret"}
|
||||
|
||||
|
||||
class SecretService:
|
||||
@staticmethod
|
||||
def encrypt(value: str) -> str:
|
||||
if not value or value.startswith(ENCRYPTED_PREFIX):
|
||||
return value
|
||||
token = SecretService._fernet().encrypt(value.encode("utf-8")).decode("ascii")
|
||||
return f"{ENCRYPTED_PREFIX}{token}"
|
||||
|
||||
@staticmethod
|
||||
def decrypt(value: str | None) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
if not value.startswith(ENCRYPTED_PREFIX):
|
||||
return value
|
||||
try:
|
||||
return SecretService._fernet().decrypt(value[len(ENCRYPTED_PREFIX) :].encode("ascii")).decode("utf-8")
|
||||
except InvalidToken as exc:
|
||||
raise RuntimeError("敏感配置无法解密,请检查 CONFIG_ENCRYPTION_KEY 是否与保存时一致") from exc
|
||||
|
||||
@staticmethod
|
||||
def masked(value: str | None) -> str:
|
||||
return MASKED_SECRET if value else ""
|
||||
|
||||
@staticmethod
|
||||
def validate_production_key() -> None:
|
||||
settings = get_settings()
|
||||
if settings.app_env.strip().lower() in {"local", "dev", "development", "docker", "test", "testing"}:
|
||||
return
|
||||
if not settings.config_encryption_key.strip():
|
||||
raise RuntimeError("生产环境必须配置 CONFIG_ENCRYPTION_KEY")
|
||||
SecretService._fernet()
|
||||
|
||||
@staticmethod
|
||||
def _fernet() -> Fernet:
|
||||
settings = get_settings()
|
||||
configured = settings.config_encryption_key.strip()
|
||||
if configured:
|
||||
try:
|
||||
return Fernet(configured.encode("ascii"))
|
||||
except (ValueError, TypeError) as exc:
|
||||
raise RuntimeError("CONFIG_ENCRYPTION_KEY 格式无效,必须是 Fernet 密钥") from exc
|
||||
fallback = base64.urlsafe_b64encode(hashlib.sha256(settings.jwt_secret_key.encode("utf-8")).digest())
|
||||
return Fernet(fallback)
|
||||
@@ -14,6 +14,7 @@ from sqlalchemy.orm import Session
|
||||
from app.core.config import get_settings
|
||||
from app.models.ai_config import SystemConfig
|
||||
from app.services.redis_client import get_sync_redis_client
|
||||
from app.services.secret_service import SecretService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -102,10 +103,8 @@ def _load_sms_config(db: Session) -> SmsProviderConfig:
|
||||
"aliyun_sms_access_key_id",
|
||||
settings.aliyun_sms_access_key_id,
|
||||
),
|
||||
aliyun_sms_access_key_secret=_config_text(
|
||||
values,
|
||||
"aliyun_sms_access_key_secret",
|
||||
settings.aliyun_sms_access_key_secret,
|
||||
aliyun_sms_access_key_secret=SecretService.decrypt(
|
||||
_config_text(values, "aliyun_sms_access_key_secret", settings.aliyun_sms_access_key_secret)
|
||||
),
|
||||
aliyun_sms_sign_name=_config_text(values, "aliyun_sms_sign_name", settings.aliyun_sms_sign_name),
|
||||
aliyun_sms_template_code=_config_text(
|
||||
|
||||
Reference in New Issue
Block a user