run.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. #!/usr/bin/env python3
  2. """
  3. 密钥过期检查定时任务 - 独立进程
  4. 从 backend-admin 拆出,单独运行。需能访问 backend-admin 的 app 包(数据库、配置、模型等)。
  5. 运行方式(在项目根目录 /Users/wuzilong/work/00code/steam 下):
  6. PYTHONPATH=backend-admin python api-key-tasks/run.py
  7. 或先安装依赖后:
  8. cd api-key-tasks && pip install -r requirements.txt
  9. PYTHONPATH=../backend-admin python run.py
  10. """
  11. import asyncio
  12. import signal
  13. import sys
  14. from pathlib import Path
  15. # 将 backend-admin 加入 path,以便 import app.*
  16. _REPO_ROOT = Path(__file__).resolve().parent.parent
  17. _BACKEND_ADMIN = _REPO_ROOT / "backend-admin"
  18. if _BACKEND_ADMIN.exists() and str(_BACKEND_ADMIN) not in sys.path:
  19. sys.path.insert(0, str(_REPO_ROOT))
  20. # 以项目根为基准时,需要能 import app -> 即 backend-admin 为当前“应用根”
  21. sys.path.insert(0, str(_BACKEND_ADMIN))
  22. from datetime import timedelta # noqa: E402
  23. from app.database import get_db # noqa: E402
  24. from app.core.logger import logger # noqa: E402
  25. from app.core.encrypt import encryptor # noqa: E402
  26. from app.models.api_key import APIKey # noqa: E402
  27. from app.models.system_config import SystemConfig # noqa: E402
  28. from app.utils.timezone import beijing_now # noqa: E402
  29. from app.utils.history import create_api_key_history, create_operation_log # noqa: E402
  30. _running = True
  31. def _sig_handler(*_):
  32. global _running
  33. _running = False
  34. async def key_expiry_check_task():
  35. """密钥过期检查:当前密钥即将过期时自动生成新密钥(与 backend-admin 原逻辑一致)"""
  36. check_interval_seconds = SystemConfig.get_config_value("key_expiry_check_interval_seconds", 60)
  37. advance_generate_minutes = SystemConfig.get_config_value("key_advance_generate_minutes", 10)
  38. logger.info(
  39. "api-key-tasks 启动密钥过期检查 (检查间隔: {}秒, 提前生成: {}分钟)",
  40. check_interval_seconds,
  41. advance_generate_minutes,
  42. )
  43. while _running:
  44. try:
  45. logger.debug("执行密钥过期检查...")
  46. db = next(get_db())
  47. try:
  48. current_key = (
  49. db.query(APIKey)
  50. .filter(APIKey.is_active == 1)
  51. .order_by(APIKey.key_version.desc())
  52. .first()
  53. )
  54. if current_key and current_key.expire_at:
  55. beijing_time = beijing_now()
  56. if beijing_time.tzinfo is not None:
  57. beijing_time_naive = beijing_time.replace(tzinfo=None)
  58. else:
  59. beijing_time_naive = beijing_time
  60. time_until_expire = current_key.expire_at - beijing_time_naive
  61. time_until_expire_minutes = time_until_expire.total_seconds() / 60
  62. if time_until_expire_minutes <= advance_generate_minutes:
  63. existing_new_key = (
  64. db.query(APIKey)
  65. .filter(
  66. APIKey.key_version == current_key.key_version + 1,
  67. APIKey.is_active == 1,
  68. )
  69. .first()
  70. )
  71. if not existing_new_key:
  72. logger.info(
  73. "检测到密钥即将过期(剩余{:.1f}分钟),开始自动生成新密钥...",
  74. time_until_expire_minutes,
  75. )
  76. new_key_pair = encryptor.generate_key_pair()
  77. new_version = current_key.key_version + 1
  78. expire_minutes = SystemConfig.get_config_value("key_expire_minutes", 60)
  79. expire_at = beijing_now() + timedelta(minutes=expire_minutes)
  80. new_key = APIKey(
  81. public_key=new_key_pair[1],
  82. private_key=new_key_pair[0],
  83. key_version=new_version,
  84. is_active=1,
  85. expire_at=expire_at,
  86. created_at=beijing_now(),
  87. updated_at=beijing_now(),
  88. )
  89. db.add(new_key)
  90. db.flush()
  91. create_api_key_history(db, new_key, "INSERT")
  92. create_operation_log(
  93. db=db,
  94. admin_id=0,
  95. module="密钥管理",
  96. action="自动密钥轮换",
  97. description=(
  98. f"自动检测密钥即将过期,生成新密钥:版本 {new_version},"
  99. f"距离过期 {time_until_expire_minutes:.1f}分钟"
  100. ),
  101. old_data={
  102. "old_version": current_key.key_version,
  103. "expire_at": current_key.expire_at.isoformat(),
  104. },
  105. new_data={
  106. "new_version": new_version,
  107. "expire_minutes": expire_minutes,
  108. },
  109. ip_address="api-key-tasks",
  110. )
  111. db.commit()
  112. logger.info(
  113. "自动生成新密钥成功:版本 {},过期时间 {}",
  114. new_version,
  115. expire_at,
  116. )
  117. else:
  118. logger.debug(
  119. "新密钥已存在(版本 {}),无需重复生成",
  120. existing_new_key.key_version,
  121. )
  122. else:
  123. logger.debug(
  124. "密钥状态正常,距离过期还有 {:.1f} 分钟",
  125. time_until_expire_minutes,
  126. )
  127. else:
  128. logger.warning("未找到激活的密钥或密钥无过期时间")
  129. finally:
  130. db.close()
  131. except asyncio.CancelledError:
  132. break
  133. except Exception as e:
  134. logger.exception("密钥过期检查任务异常: {}", e)
  135. # 等待下一轮
  136. try:
  137. await asyncio.wait_for(asyncio.shield(asyncio.sleep(check_interval_seconds)), timeout=None)
  138. except asyncio.CancelledError:
  139. break
  140. logger.info("api-key-tasks 密钥过期检查任务已退出")
  141. async def main():
  142. global _running
  143. for sig in (signal.SIGINT, signal.SIGTERM):
  144. try:
  145. signal.signal(sig, _sig_handler)
  146. except (ValueError, OSError):
  147. pass # Windows / 非主线程等可能不支持
  148. task = asyncio.create_task(key_expiry_check_task())
  149. try:
  150. await task
  151. except asyncio.CancelledError:
  152. _running = False
  153. task.cancel()
  154. try:
  155. await task
  156. except asyncio.CancelledError:
  157. pass
  158. if __name__ == "__main__":
  159. logger.info("启动 api-key-tasks(密钥定时生成任务)")
  160. asyncio.run(main())