| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- """
- 密钥管理服务
- backend-client 仅允许读取密钥,不得修改 api_keys 表(含 is_active)。
- 密钥的生成与 is_active 管理由 backend-admin / api-key-tasks 负责。
- """
- import secrets
- import hashlib
- from datetime import datetime, timedelta
- from typing import Optional, Tuple, Dict, Any
- from sqlalchemy import or_
- from sqlalchemy.orm import Session
- from cryptography.hazmat.primitives import serialization
- from cryptography.hazmat.primitives.asymmetric import rsa
- from cryptography.hazmat.backends import default_backend
- from app.models.api_key import APIKey
- from app.models.system_config import SystemConfig
- from app.core.logger import logger
- from app.core.encrypt import encryptor
- from app.utils.timezone import beijing_now_naive
- def _effective_key_query(query):
- """当前有效密钥条件:未删除且未过期(仅读,不写 is_active)"""
- now = beijing_now_naive()
- return query.filter(
- APIKey.is_deleted == 0,
- or_(APIKey.expire_at.is_(None), APIKey.expire_at > now)
- ).order_by(APIKey.key_version.desc())
- class KeyService:
- """密钥管理服务(只读 api_keys,不修改 is_active)"""
- def __init__(self, db: Session):
- self.db = db
- def get_active_public_key(self) -> Tuple[bool, str, Dict[str, Any]]:
- """
- 获取当前有效的RSA公钥(按未删除、未过期取版本最大的一条,不修改 is_active)
- """
- try:
- api_key = _effective_key_query(self.db.query(APIKey)).first()
- if not api_key:
- return False, "未找到有效的API密钥", {}
- return True, "获取成功", {
- "public_key": api_key.public_key,
- "key_version": api_key.key_version,
- "expire_at": api_key.expire_at.isoformat() if api_key.expire_at else None
- }
- except Exception as e:
- logger.exception(f"获取公钥失败: {e}")
- return False, f"获取公钥失败: {str(e)}", {}
- def get_active_key_pair(self) -> Tuple[bool, str, Dict[str, Any]]:
- """
- 获取当前有效的RSA密钥对(公钥+私钥),仅读,不修改 is_active。
- """
- try:
- api_key = _effective_key_query(self.db.query(APIKey)).first()
- if not api_key:
- return False, "未找到有效的API密钥", {}
- return True, "获取成功", {
- "public_key": api_key.public_key,
- "private_key": api_key.private_key,
- "key_version": api_key.key_version,
- }
- except Exception as e:
- logger.exception(f"获取密钥对失败: {e}")
- return False, str(e), {}
- def get_current_key_version(self) -> Tuple[bool, str, Dict[str, Any]]:
- """获取当前有效密钥版本(仅读)"""
- try:
- api_key = _effective_key_query(self.db.query(APIKey)).first()
- if not api_key:
- return False, "未找到有效的API密钥", {}
- return True, "获取成功", {
- "key_version": api_key.key_version,
- "expire_at": api_key.expire_at.isoformat() if api_key.expire_at else None
- }
- except Exception as e:
- logger.exception(f"获取密钥版本失败: {e}")
- return False, f"获取密钥版本失败: {str(e)}", {}
-
- def generate_new_key_pair(self) -> Tuple[bool, str, Dict[str, Any]]:
- """
- 生成新的RSA密钥对
-
- Returns:
- (success, message, data)
- """
- try:
- # 获取当前最大版本号
- max_version = self.db.query(APIKey.key_version).filter(
- APIKey.is_deleted == 0
- ).order_by(APIKey.key_version.desc()).first()
-
- new_version = (max_version[0] + 1) if max_version else 1
-
- # 生成RSA密钥对
- private_key = rsa.generate_private_key(
- public_exponent=65537,
- key_size=2048,
- backend=default_backend()
- )
-
- public_key = private_key.public_key()
-
- # 序列化公钥
- public_key_pem = public_key.public_bytes(
- encoding=serialization.Encoding.PEM,
- format=serialization.PublicFormat.SubjectPublicKeyInfo
- ).decode('utf-8')
-
- # 序列化私钥
- private_key_pem = private_key.private_bytes(
- encoding=serialization.Encoding.PEM,
- format=serialization.PrivateFormat.PKCS8,
- encryption_algorithm=serialization.NoEncryption()
- ).decode('utf-8')
-
- # 计算过期时间 (10天后)
- expire_at = beijing_now_naive() + timedelta(days=10)
-
- # 创建新密钥记录
- api_key = APIKey(
- public_key=public_key_pem,
- private_key=private_key_pem,
- key_version=new_version,
- is_active=1,
- expire_at=expire_at
- )
-
- self.db.add(api_key)
- self.db.commit()
-
- logger.info(f"生成新的RSA密钥对,版本: {new_version}")
-
- return True, "密钥生成成功", {
- "key_version": new_version,
- "expire_at": expire_at.isoformat()
- }
-
- except Exception as e:
- self.db.rollback()
- logger.exception(f"生成密钥对失败: {e}")
- return False, f"生成密钥对失败: {str(e)}", {}
-
- def rotate_keys(self) -> Tuple[bool, str, Dict[str, Any]]:
- """
- 轮换密钥:仅生成新密钥,不修改已有密钥的 is_active。
- 密钥的停用与轮换策略由 backend-admin / api-key-tasks 负责。
- """
- try:
- success, message, data = self.generate_new_key_pair()
- if success:
- self.db.commit()
- return True, "密钥生成成功", data
- else:
- self.db.rollback()
- return False, message, {}
- except Exception as e:
- self.db.rollback()
- logger.exception(f"密钥轮换失败: {e}")
- return False, f"密钥轮换失败: {str(e)}", {}
-
- def get_client_configs(self) -> Tuple[bool, str, Dict[str, Any]]:
- """
- 获取客户端配置
-
- Returns:
- (success, message, data)
- """
- try:
- # 获取所有客户端配置
- configs = self.db.query(SystemConfig).filter(
- SystemConfig.app_name.in_(['backend-client', 'backend-selection']),
- SystemConfig.is_deleted == 0
- ).all()
-
- config_dict = {}
- for config in configs:
- config_dict[config.config_key] = {
- "value": config.config_value,
- "type": config.config_type,
- "description": config.description
- }
-
- # 如果没有配置,返回默认配置
- if not config_dict:
- config_dict = self._get_default_configs()
-
- return True, "获取成功", config_dict
-
- except Exception as e:
- logger.exception(f"获取客户端配置失败: {e}")
- return False, f"获取配置失败: {str(e)}", {}
-
- def _get_default_configs(self) -> Dict[str, Any]:
- """获取默认客户端配置"""
- return {
- "heartbeat_interval_seconds": {
- "value": "10",
- "type": "int",
- "description": "心跳检测间隔秒数"
- },
- "max_heartbeat_failures": {
- "value": "100",
- "type": "int",
- "description": "心跳最大失败次数"
- },
- "session_renewal_threshold": {
- "value": "1800",
- "type": "int",
- "description": "Session续期阈值(秒)"
- },
- "session_extension_duration": {
- "value": "3600",
- "type": "int",
- "description": "Session延长时间(秒)"
- },
- "session_default_expiry_seconds": {
- "value": "864000",
- "type": "int",
- "description": "Session默认有效期(秒)"
- },
- "timestamp_tolerance_seconds": {
- "value": "300",
- "type": "int",
- "description": "时间戳容忍秒数"
- },
- "prevent_duplicate_login": {
- "value": "true",
- "type": "bool",
- "description": "是否防止重复登录"
- },
- "max_login_attempts": {
- "value": "5",
- "type": "int",
- "description": "最大登录失败次数"
- },
- "login_lockout_minutes": {
- "value": "60",
- "type": "int",
- "description": "登录锁定分钟数"
- },
- "announcement_cache_seconds": {
- "value": "300",
- "type": "int",
- "description": "公告缓存秒数"
- },
- "key_sync_interval_seconds": {
- "value": "60",
- "type": "int",
- "description": "密钥同步间隔秒数"
- }
- }
-
- def get_config_by_key(self, key: str) -> Optional[str]:
- """
- 根据键名获取配置值
-
- Args:
- key: 配置键名
-
- Returns:
- 配置值或None
- """
- try:
- config = self.db.query(SystemConfig).filter(
- SystemConfig.config_key == key,
- SystemConfig.app_name == 'backend-client',
- SystemConfig.is_deleted == 0
- ).first()
-
- return config.config_value if config else None
-
- except Exception as e:
- logger.exception(f"获取配置失败: {e}")
- return None
-
- def update_config(self, key: str, value: str, config_type: str = "string",
- description: str = None) -> Tuple[bool, str]:
- """
- 更新配置
-
- Args:
- key: 配置键名
- value: 配置值
- config_type: 配置类型
- description: 配置描述
-
- Returns:
- (success, message)
- """
- try:
- config = self.db.query(SystemConfig).filter(
- SystemConfig.config_key == key,
- SystemConfig.app_name == 'backend-client'
- ).first()
-
- if config:
- config.config_value = value
- config.config_type = config_type
- if description:
- config.description = description
- else:
- config = SystemConfig(
- config_key=key,
- config_value=value,
- config_type=config_type,
- app_name='backend-client',
- description=description
- )
- self.db.add(config)
-
- self.db.commit()
-
- logger.info(f"更新客户端配置: {key} = {value}")
-
- return True, "配置更新成功"
-
- except Exception as e:
- self.db.rollback()
- logger.exception(f"更新配置失败: {e}")
- return False, f"更新配置失败: {str(e)}"
-
- def check_and_rotate_expired_keys(self) -> Tuple[bool, str, int]:
- """
- 客户端不修改 api_keys;过期密钥的轮换由 api-key-tasks 定时任务负责。
- Returns:
- (success, message, 0)
- """
- try:
- self.db.commit()
- return True, "密钥轮换由 api-key-tasks 负责", 0
- except Exception as e:
- self.db.rollback()
- logger.exception(f"检查密钥失败: {e}")
- return False, f"检查失败: {str(e)}", 0
|