""" 密钥管理服务 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 == 'backend-client', 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": "86400", "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