key_rotation_service.py 7.4 KB

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