key_rotation_service.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. """
  2. 密钥轮换服务
  3. """
  4. import time
  5. from datetime import datetime, timedelta
  6. from sqlalchemy.orm import Session
  7. from sqlalchemy import and_, or_
  8. from app.database import get_db
  9. from app.models.api_key import APIKey
  10. from app.models.system_config import SystemConfig
  11. from app.core.encrypt import encryptor
  12. from app.core.logger import logger
  13. from app.utils.timezone import beijing_now
  14. class KeyRotationService:
  15. """密钥轮换服务"""
  16. @staticmethod
  17. def get_config_dict() -> dict:
  18. """获取密钥轮换相关配置"""
  19. return {
  20. 'key_overlap_minutes': SystemConfig.get_config_value('key_overlap_minutes', 30),
  21. 'key_expire_minutes': SystemConfig.get_config_value('key_expire_minutes', 60),
  22. }
  23. @staticmethod
  24. def should_rotate_key(db: Session) -> bool:
  25. """
  26. 检查是否需要轮换密钥
  27. 轮换条件:
  28. 1. 当前没有激活的密钥
  29. 2. 当前密钥即将过期(剩余时间少于重叠时间)
  30. """
  31. try:
  32. config = KeyRotationService.get_config_dict()
  33. # 检查是否有激活的有效密钥(主is_active,辅expire_at)
  34. active_key = db.query(APIKey).filter(
  35. and_(
  36. APIKey.is_active == 1,
  37. APIKey.is_deleted == 0,
  38. APIKey.expire_at > beijing_now() # 未过期
  39. )
  40. ).order_by(APIKey.key_version.desc()).first()
  41. if not active_key:
  42. logger.info("没有激活的密钥,需要生成新密钥")
  43. return True
  44. # 检查是否即将过期
  45. if active_key.expire_at:
  46. current_time = beijing_now()
  47. # 确保两个时间都是相同类型
  48. if active_key.expire_at.tzinfo is None:
  49. # 如果数据库时间是naive的,转换为aware
  50. from datetime import timezone, timedelta
  51. expire_time = active_key.expire_at.replace(tzinfo=timezone(timedelta(seconds=28800))) # 北京时间
  52. else:
  53. expire_time = active_key.expire_at
  54. time_until_expire = expire_time - current_time
  55. overlap_minutes = config.get('key_overlap_minutes', 30)
  56. if time_until_expire.total_seconds() < (overlap_minutes * 60):
  57. logger.info(f"当前密钥即将过期(剩余{time_until_expire.total_seconds()/60:.1f}分钟),需要轮换")
  58. return True
  59. return False
  60. except Exception as e:
  61. logger.exception("检查密钥轮换条件失败")
  62. return True # 出错时认为需要轮换
  63. @staticmethod
  64. def rotate_key(db: Session) -> tuple[bool, str, str]:
  65. """
  66. 执行密钥轮换
  67. Returns:
  68. tuple: (是否成功, 新公钥, 错误信息)
  69. """
  70. try:
  71. config = KeyRotationService.get_config_dict()
  72. # 生成新密钥对
  73. private_pem, public_pem = encryptor.generate_key_pair()
  74. # 获取下一个版本号
  75. max_version = db.query(APIKey.key_version).filter(APIKey.is_deleted == 0).order_by(APIKey.key_version.desc()).first()
  76. next_version = (max_version[0] + 1) if max_version else 1
  77. # 计算过期时间
  78. expire_minutes = config.get('key_expire_minutes', 60)
  79. expire_at = beijing_now() + timedelta(minutes=expire_minutes)
  80. # 保存新密钥
  81. new_key = APIKey(
  82. public_key=public_pem,
  83. private_key=private_pem,
  84. key_version=next_version,
  85. is_active=1,
  86. expire_at=expire_at,
  87. created_at=beijing_now()
  88. )
  89. # 保存新密钥
  90. db.add(new_key)
  91. db.commit()
  92. db.refresh(new_key)
  93. # 加载新密钥到加密器
  94. encryptor.load_private_key(private_pem, next_version)
  95. logger.info(f"密钥轮换成功,新版本: {next_version}")
  96. return True, public_pem, ""
  97. except Exception as e:
  98. db.rollback()
  99. error_msg = f"密钥轮换失败: {str(e)}"
  100. logger.exception("密钥轮换失败")
  101. return False, "", error_msg
  102. @staticmethod
  103. def cleanup_expired_keys(db: Session) -> int:
  104. """
  105. 清理过期的密钥
  106. Returns:
  107. int: 清理的密钥数量
  108. """
  109. try:
  110. # 删除过期超过 overlap_minutes 的密钥
  111. config = KeyRotationService.get_config_dict()
  112. overlap_minutes = config.get('key_overlap_minutes', 30)
  113. cutoff_time = beijing_now() - timedelta(minutes=overlap_minutes)
  114. result = db.query(APIKey).filter(
  115. and_(
  116. APIKey.is_active == 0, # 只删除非激活的密钥
  117. APIKey.is_deleted == 0,
  118. APIKey.expire_at < cutoff_time
  119. )
  120. ).update({
  121. "is_deleted": 1,
  122. "updated_at": beijing_now()
  123. })
  124. if result > 0:
  125. db.commit()
  126. logger.info(f"清理了 {result} 个过期的密钥")
  127. return result
  128. except Exception as e:
  129. db.rollback()
  130. logger.exception("清理过期密钥失败")
  131. return 0
  132. @staticmethod
  133. def get_active_key_version(db: Session) -> int:
  134. """获取当前激活密钥的版本(主is_active,辅expire_at)"""
  135. try:
  136. active_key = db.query(APIKey).filter(
  137. and_(
  138. APIKey.is_active == 1,
  139. APIKey.is_deleted == 0,
  140. APIKey.expire_at > beijing_now()
  141. )
  142. ).order_by(APIKey.key_version.desc()).first()
  143. return active_key.key_version if active_key else 0
  144. except Exception as e:
  145. logger.exception("获取激活密钥版本失败")
  146. return 0
  147. @staticmethod
  148. def get_public_key_by_version(db: Session, version: int) -> str:
  149. """根据版本获取公钥"""
  150. try:
  151. api_key = db.query(APIKey).filter(
  152. and_(
  153. APIKey.key_version == version,
  154. APIKey.is_deleted == 0
  155. )
  156. ).first()
  157. return api_key.public_key if api_key else ""
  158. except Exception as e:
  159. logger.exception(f"获取版本 {version} 的公钥失败")
  160. return ""
  161. @staticmethod
  162. def initialize_keys(db: Session) -> bool:
  163. """初始化密钥系统"""
  164. try:
  165. # 检查是否已有激活密钥
  166. if not KeyRotationService.should_rotate_key(db):
  167. logger.info("密钥系统已初始化")
  168. return True
  169. # 执行密钥轮换
  170. success, _, error_msg = KeyRotationService.rotate_key(db)
  171. if success:
  172. logger.info("密钥系统初始化成功")
  173. return True
  174. else:
  175. logger.error(f"密钥系统初始化失败: {error_msg}")
  176. return False
  177. except Exception as e:
  178. logger.exception("密钥系统初始化异常")
  179. return False