nonce_store.py 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
  1. """
  2. DB-first nonce dedup helper with Redis fallback.
  3. Table expected: api_nonces(nonce PK, endpoint, session_id, created_at, expires_at)
  4. """
  5. from typing import Optional
  6. from datetime import datetime, timedelta
  7. from sqlalchemy import text
  8. from sqlalchemy.exc import IntegrityError
  9. from app.core.redis_cache import redis_cache
  10. from app.core.logger import logger
  11. from app.utils.timezone import beijing_now_naive, beijing_now
  12. async def check_and_set_nonce(db, nonce: str, endpoint: str, session_id: Optional[str], ttl_seconds: int) -> bool:
  13. """
  14. Try DB insert first (primary). If insert succeeds => first use => return True.
  15. If insert fails due to IntegrityError => duplicate => return False.
  16. If DB insert raises other errors, attempt Redis fallback: SET NX with ttl.
  17. If Redis available, use it (True if set, False if existed). If both fail, raise.
  18. """
  19. try:
  20. expires_at = beijing_now_naive() + timedelta(seconds=ttl_seconds)
  21. sql = text(
  22. "INSERT INTO api_nonces (nonce, endpoint, session_id, created_at, expires_at) "
  23. "VALUES (:nonce, :endpoint, :session_id, :created_at, :expires_at)"
  24. )
  25. db.execute(sql, {
  26. "nonce": nonce,
  27. "endpoint": endpoint,
  28. "session_id": session_id,
  29. "created_at": beijing_now_naive(),
  30. "expires_at": expires_at
  31. })
  32. db.commit()
  33. return True
  34. except IntegrityError:
  35. # duplicate
  36. try:
  37. db.rollback()
  38. except Exception:
  39. pass
  40. return False
  41. except Exception as e:
  42. logger.exception("DB 插入 nonce 失败,尝试 Redis 回退")
  43. try:
  44. # Redis fallback (async)
  45. key = f"nonce:{nonce}"
  46. ttl = ttl_seconds
  47. ok = await redis_cache.set(key, {"ts": int(beijing_now().timestamp())}, ttl)
  48. # redis_cache.set returns truthy on success; if key already exists, exists() would be True
  49. if not ok:
  50. # check exists -> duplicate
  51. exists = await redis_cache.exists(key)
  52. if exists:
  53. return False
  54. return True
  55. except Exception:
  56. logger.exception("Redis 回退也失败,nonce 去重无法完成")
  57. # Conservative: treat as failure to set -> raise to let caller decide to reject
  58. raise