key_rotation_service.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  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.core.encrypt import encryptor
  11. from app.core.logger import logger
  12. from app.utils.timezone import beijing_now
  13. from app.utils.history import create_api_key_history
  14. # 密钥轮换配置(硬编码)
  15. KEY_OVERLAP_MINUTES = 30 # 密钥重叠时间(分钟)
  16. KEY_EXPIRE_MINUTES = 60 # 密钥过期时间(分钟)
  17. class KeyRotationService:
  18. """密钥轮换服务"""
  19. @staticmethod
  20. def get_config_dict() -> dict:
  21. """获取密钥轮换相关配置"""
  22. return {
  23. 'key_overlap_minutes': KEY_OVERLAP_MINUTES,
  24. 'key_expire_minutes': KEY_EXPIRE_MINUTES,
  25. }
  26. @staticmethod
  27. def should_rotate_key(db: Session) -> bool:
  28. """
  29. 检查是否需要轮换密钥
  30. 轮换条件:
  31. 1. 当前没有激活的密钥
  32. 2. 当前密钥即将过期(剩余时间少于重叠时间)
  33. """
  34. try:
  35. config = KeyRotationService.get_config_dict()
  36. # 检查是否有激活的有效密钥(主is_active,辅expire_at)
  37. active_key = db.query(APIKey).filter(
  38. and_(
  39. APIKey.is_active == 1,
  40. APIKey.is_deleted == 0,
  41. APIKey.expire_at > beijing_now() # 未过期
  42. )
  43. ).order_by(APIKey.key_version.desc()).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.add(new_key)
  94. db.commit()
  95. db.refresh(new_key)
  96. # 加载新密钥到加密器
  97. encryptor.load_private_key(private_pem, next_version)
  98. logger.info(f"密钥轮换成功,新版本: {next_version}")
  99. # 同步下发 signing_public_key 到运行时配置(供心跳/keys接口下发)
  100. try:
  101. setattr(settings, "SIGNING_PUBLIC_KEY", public_pem)
  102. except Exception:
  103. logger.exception("设置运行时 signing_public_key 失败")
  104. return True, public_pem, ""
  105. except Exception as e:
  106. db.rollback()
  107. error_msg = f"密钥轮换失败: {str(e)}"
  108. logger.exception("密钥轮换失败")
  109. return False, "", error_msg
  110. @staticmethod
  111. def cleanup_expired_keys(db: Session) -> int:
  112. """
  113. 清理过期的密钥
  114. Returns:
  115. int: 清理的密钥数量
  116. """
  117. try:
  118. config = KeyRotationService.get_config_dict()
  119. overlap_minutes = config.get('key_overlap_minutes', 30)
  120. cutoff_time = beijing_now() - timedelta(minutes=overlap_minutes)
  121. # 先写历史表,再改/删:查出即将被软删除的密钥,逐条写入 api_keys_his
  122. keys_to_clean = db.query(APIKey).filter(
  123. and_(
  124. APIKey.is_active == 0,
  125. APIKey.is_deleted == 0,
  126. APIKey.expire_at < cutoff_time
  127. )
  128. ).all()
  129. for key in keys_to_clean:
  130. create_api_key_history(db, key, "DELETE")
  131. result = db.query(APIKey).filter(
  132. and_(
  133. APIKey.is_active == 0, # 只删除非激活的密钥
  134. APIKey.is_deleted == 0,
  135. APIKey.expire_at < cutoff_time
  136. )
  137. ).update({
  138. "is_deleted": 1,
  139. "updated_at": beijing_now()
  140. })
  141. if result > 0:
  142. db.commit()
  143. logger.info(f"清理了 {result} 个过期的密钥")
  144. return result
  145. except Exception as e:
  146. db.rollback()
  147. logger.exception("清理过期密钥失败")
  148. return 0
  149. @staticmethod
  150. def get_active_key_version(db: Session) -> int:
  151. """获取当前激活密钥的版本(主is_active,辅expire_at)"""
  152. try:
  153. active_key = db.query(APIKey).filter(
  154. and_(
  155. APIKey.is_active == 1,
  156. APIKey.is_deleted == 0,
  157. APIKey.expire_at > beijing_now()
  158. )
  159. ).order_by(APIKey.key_version.desc()).first()
  160. return active_key.key_version if active_key else 0
  161. except Exception as e:
  162. logger.exception("获取激活密钥版本失败")
  163. return 0
  164. @staticmethod
  165. def get_public_key_by_version(db: Session, version: int) -> str:
  166. """根据版本获取公钥"""
  167. try:
  168. api_key = db.query(APIKey).filter(
  169. and_(
  170. APIKey.key_version == version,
  171. APIKey.is_deleted == 0
  172. )
  173. ).first()
  174. return api_key.public_key if api_key else ""
  175. except Exception as e:
  176. logger.exception(f"获取版本 {version} 的公钥失败")
  177. return ""
  178. @staticmethod
  179. def initialize_keys(db: Session) -> bool:
  180. """初始化密钥系统"""
  181. try:
  182. # 检查是否已有激活密钥
  183. if not KeyRotationService.should_rotate_key(db):
  184. logger.info("密钥系统已初始化")
  185. return True
  186. # 执行密钥轮换
  187. success, _, error_msg = KeyRotationService.rotate_key(db)
  188. if success:
  189. logger.info("密钥系统初始化成功")
  190. return True
  191. else:
  192. logger.error(f"密钥系统初始化失败: {error_msg}")
  193. return False
  194. except Exception as e:
  195. logger.exception("密钥系统初始化异常")
  196. return False