key_service.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. """
  2. 密钥管理服务
  3. backend-client 仅允许读取密钥,不得修改 api_keys 表(含 is_active)。
  4. 密钥的生成与 is_active 管理由 backend-admin / api-key-tasks 负责。
  5. """
  6. import secrets
  7. import hashlib
  8. from datetime import datetime, timedelta
  9. from typing import Optional, Tuple, Dict, Any
  10. from sqlalchemy import or_
  11. from sqlalchemy.orm import Session
  12. from cryptography.hazmat.primitives import serialization
  13. from cryptography.hazmat.primitives.asymmetric import rsa
  14. from cryptography.hazmat.backends import default_backend
  15. from app.models.api_key import APIKey
  16. from app.models.system_config import SystemConfig
  17. from app.core.logger import logger
  18. from app.core.encrypt import encryptor
  19. from app.utils.timezone import beijing_now_naive
  20. def _effective_key_query(query):
  21. """当前有效密钥条件:未删除且未过期(仅读,不写 is_active)"""
  22. now = beijing_now_naive()
  23. return query.filter(
  24. APIKey.is_deleted == 0,
  25. or_(APIKey.expire_at.is_(None), APIKey.expire_at > now)
  26. ).order_by(APIKey.key_version.desc())
  27. class KeyService:
  28. """密钥管理服务(只读 api_keys,不修改 is_active)"""
  29. def __init__(self, db: Session):
  30. self.db = db
  31. def get_active_public_key(self) -> Tuple[bool, str, Dict[str, Any]]:
  32. """
  33. 获取当前有效的RSA公钥(按未删除、未过期取版本最大的一条,不修改 is_active)
  34. """
  35. try:
  36. api_key = _effective_key_query(self.db.query(APIKey)).first()
  37. if not api_key:
  38. return False, "未找到有效的API密钥", {}
  39. return True, "获取成功", {
  40. "public_key": api_key.public_key,
  41. "key_version": api_key.key_version,
  42. "expire_at": api_key.expire_at.isoformat() if api_key.expire_at else None
  43. }
  44. except Exception as e:
  45. logger.exception(f"获取公钥失败: {e}")
  46. return False, f"获取公钥失败: {str(e)}", {}
  47. def get_active_key_pair(self) -> Tuple[bool, str, Dict[str, Any]]:
  48. """
  49. 获取当前有效的RSA密钥对(公钥+私钥),仅读,不修改 is_active。
  50. """
  51. try:
  52. api_key = _effective_key_query(self.db.query(APIKey)).first()
  53. if not api_key:
  54. return False, "未找到有效的API密钥", {}
  55. return True, "获取成功", {
  56. "public_key": api_key.public_key,
  57. "private_key": api_key.private_key,
  58. "key_version": api_key.key_version,
  59. }
  60. except Exception as e:
  61. logger.exception(f"获取密钥对失败: {e}")
  62. return False, str(e), {}
  63. def get_current_key_version(self) -> Tuple[bool, str, Dict[str, Any]]:
  64. """获取当前有效密钥版本(仅读)"""
  65. try:
  66. api_key = _effective_key_query(self.db.query(APIKey)).first()
  67. if not api_key:
  68. return False, "未找到有效的API密钥", {}
  69. return True, "获取成功", {
  70. "key_version": api_key.key_version,
  71. "expire_at": api_key.expire_at.isoformat() if api_key.expire_at else None
  72. }
  73. except Exception as e:
  74. logger.exception(f"获取密钥版本失败: {e}")
  75. return False, f"获取密钥版本失败: {str(e)}", {}
  76. def generate_new_key_pair(self) -> Tuple[bool, str, Dict[str, Any]]:
  77. """
  78. 生成新的RSA密钥对
  79. Returns:
  80. (success, message, data)
  81. """
  82. try:
  83. # 获取当前最大版本号
  84. max_version = self.db.query(APIKey.key_version).filter(
  85. APIKey.is_deleted == 0
  86. ).order_by(APIKey.key_version.desc()).first()
  87. new_version = (max_version[0] + 1) if max_version else 1
  88. # 生成RSA密钥对
  89. private_key = rsa.generate_private_key(
  90. public_exponent=65537,
  91. key_size=2048,
  92. backend=default_backend()
  93. )
  94. public_key = private_key.public_key()
  95. # 序列化公钥
  96. public_key_pem = public_key.public_bytes(
  97. encoding=serialization.Encoding.PEM,
  98. format=serialization.PublicFormat.SubjectPublicKeyInfo
  99. ).decode('utf-8')
  100. # 序列化私钥
  101. private_key_pem = private_key.private_bytes(
  102. encoding=serialization.Encoding.PEM,
  103. format=serialization.PrivateFormat.PKCS8,
  104. encryption_algorithm=serialization.NoEncryption()
  105. ).decode('utf-8')
  106. # 计算过期时间 (10天后)
  107. expire_at = beijing_now_naive() + timedelta(days=10)
  108. # 创建新密钥记录
  109. api_key = APIKey(
  110. public_key=public_key_pem,
  111. private_key=private_key_pem,
  112. key_version=new_version,
  113. is_active=1,
  114. expire_at=expire_at
  115. )
  116. self.db.add(api_key)
  117. self.db.commit()
  118. logger.info(f"生成新的RSA密钥对,版本: {new_version}")
  119. return True, "密钥生成成功", {
  120. "key_version": new_version,
  121. "expire_at": expire_at.isoformat()
  122. }
  123. except Exception as e:
  124. self.db.rollback()
  125. logger.exception(f"生成密钥对失败: {e}")
  126. return False, f"生成密钥对失败: {str(e)}", {}
  127. def rotate_keys(self) -> Tuple[bool, str, Dict[str, Any]]:
  128. """
  129. 轮换密钥:仅生成新密钥,不修改已有密钥的 is_active。
  130. 密钥的停用与轮换策略由 backend-admin / api-key-tasks 负责。
  131. """
  132. try:
  133. success, message, data = self.generate_new_key_pair()
  134. if success:
  135. self.db.commit()
  136. return True, "密钥生成成功", data
  137. else:
  138. self.db.rollback()
  139. return False, message, {}
  140. except Exception as e:
  141. self.db.rollback()
  142. logger.exception(f"密钥轮换失败: {e}")
  143. return False, f"密钥轮换失败: {str(e)}", {}
  144. def get_client_configs(self) -> Tuple[bool, str, Dict[str, Any]]:
  145. """
  146. 获取客户端配置
  147. Returns:
  148. (success, message, data)
  149. """
  150. try:
  151. # 获取所有客户端配置
  152. configs = self.db.query(SystemConfig).filter(
  153. SystemConfig.app_name.in_(['backend-client', 'backend-selection']),
  154. SystemConfig.is_deleted == 0
  155. ).all()
  156. config_dict = {}
  157. for config in configs:
  158. config_dict[config.config_key] = {
  159. "value": config.config_value,
  160. "type": config.config_type,
  161. "description": config.description
  162. }
  163. # 如果没有配置,返回默认配置
  164. if not config_dict:
  165. config_dict = self._get_default_configs()
  166. return True, "获取成功", config_dict
  167. except Exception as e:
  168. logger.exception(f"获取客户端配置失败: {e}")
  169. return False, f"获取配置失败: {str(e)}", {}
  170. def _get_default_configs(self) -> Dict[str, Any]:
  171. """获取默认客户端配置"""
  172. return {
  173. "heartbeat_interval_seconds": {
  174. "value": "10",
  175. "type": "int",
  176. "description": "心跳检测间隔秒数"
  177. },
  178. "max_heartbeat_failures": {
  179. "value": "100",
  180. "type": "int",
  181. "description": "心跳最大失败次数"
  182. },
  183. "session_renewal_threshold": {
  184. "value": "1800",
  185. "type": "int",
  186. "description": "Session续期阈值(秒)"
  187. },
  188. "session_extension_duration": {
  189. "value": "3600",
  190. "type": "int",
  191. "description": "Session延长时间(秒)"
  192. },
  193. "session_default_expiry_seconds": {
  194. "value": "864000",
  195. "type": "int",
  196. "description": "Session默认有效期(秒)"
  197. },
  198. "timestamp_tolerance_seconds": {
  199. "value": "300",
  200. "type": "int",
  201. "description": "时间戳容忍秒数"
  202. },
  203. "prevent_duplicate_login": {
  204. "value": "true",
  205. "type": "bool",
  206. "description": "是否防止重复登录"
  207. },
  208. "max_login_attempts": {
  209. "value": "5",
  210. "type": "int",
  211. "description": "最大登录失败次数"
  212. },
  213. "login_lockout_minutes": {
  214. "value": "60",
  215. "type": "int",
  216. "description": "登录锁定分钟数"
  217. },
  218. "announcement_cache_seconds": {
  219. "value": "300",
  220. "type": "int",
  221. "description": "公告缓存秒数"
  222. },
  223. "key_sync_interval_seconds": {
  224. "value": "60",
  225. "type": "int",
  226. "description": "密钥同步间隔秒数"
  227. }
  228. }
  229. def get_config_by_key(self, key: str) -> Optional[str]:
  230. """
  231. 根据键名获取配置值
  232. Args:
  233. key: 配置键名
  234. Returns:
  235. 配置值或None
  236. """
  237. try:
  238. config = self.db.query(SystemConfig).filter(
  239. SystemConfig.config_key == key,
  240. SystemConfig.app_name == 'backend-client',
  241. SystemConfig.is_deleted == 0
  242. ).first()
  243. return config.config_value if config else None
  244. except Exception as e:
  245. logger.exception(f"获取配置失败: {e}")
  246. return None
  247. def update_config(self, key: str, value: str, config_type: str = "string",
  248. description: str = None) -> Tuple[bool, str]:
  249. """
  250. 更新配置
  251. Args:
  252. key: 配置键名
  253. value: 配置值
  254. config_type: 配置类型
  255. description: 配置描述
  256. Returns:
  257. (success, message)
  258. """
  259. try:
  260. config = self.db.query(SystemConfig).filter(
  261. SystemConfig.config_key == key,
  262. SystemConfig.app_name == 'backend-client'
  263. ).first()
  264. if config:
  265. config.config_value = value
  266. config.config_type = config_type
  267. if description:
  268. config.description = description
  269. else:
  270. config = SystemConfig(
  271. config_key=key,
  272. config_value=value,
  273. config_type=config_type,
  274. app_name='backend-client',
  275. description=description
  276. )
  277. self.db.add(config)
  278. self.db.commit()
  279. logger.info(f"更新客户端配置: {key} = {value}")
  280. return True, "配置更新成功"
  281. except Exception as e:
  282. self.db.rollback()
  283. logger.exception(f"更新配置失败: {e}")
  284. return False, f"更新配置失败: {str(e)}"
  285. def check_and_rotate_expired_keys(self) -> Tuple[bool, str, int]:
  286. """
  287. 客户端不修改 api_keys;过期密钥的轮换由 api-key-tasks 定时任务负责。
  288. Returns:
  289. (success, message, 0)
  290. """
  291. try:
  292. self.db.commit()
  293. return True, "密钥轮换由 api-key-tasks 负责", 0
  294. except Exception as e:
  295. self.db.rollback()
  296. logger.exception(f"检查密钥失败: {e}")
  297. return False, f"检查失败: {str(e)}", 0