mp_webview_ticket.py 2.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. """小程序 WebView 一次性短时 ticket(不把 JWT 放进 URL)。"""
  2. from __future__ import annotations
  3. import json
  4. from typing import Callable, Optional, TypeVar
  5. from redis import exceptions as redis_exceptions
  6. from app.core.redis_client import get_sync_redis_client
  7. from app.core.utils import generate_uuid
  8. TICKET_TTL_SEC = 60
  9. KEY_PREFIX = "mp_wv_ticket:"
  10. _GETDEL_LUA = """
  11. local val = redis.call('GET', KEYS[1])
  12. if val then
  13. redis.call('DEL', KEYS[1])
  14. end
  15. return val
  16. """
  17. T = TypeVar("T")
  18. def _key(ticket: str) -> str:
  19. return f"{KEY_PREFIX}{ticket}"
  20. def _atomic_getdel(r, key: str):
  21. if hasattr(r, "getdel"):
  22. return r.getdel(key)
  23. return r.eval(_GETDEL_LUA, 1, key)
  24. def _redis_call(fn: Callable[..., T], unavailable_msg: str) -> T:
  25. r = get_sync_redis_client()
  26. if r is None:
  27. raise RuntimeError(unavailable_msg)
  28. try:
  29. return fn(r)
  30. except (
  31. redis_exceptions.TimeoutError,
  32. redis_exceptions.ConnectionError,
  33. redis_exceptions.RedisError,
  34. ) as exc:
  35. raise RuntimeError(unavailable_msg) from exc
  36. def issue_ticket(user_id: str, session_id: str, refresh_jti: str = "") -> str:
  37. def _issue(r) -> str:
  38. ticket = generate_uuid()
  39. body = json.dumps(
  40. {"userId": user_id, "sessionId": session_id, "refreshJti": refresh_jti or ""},
  41. ensure_ascii=False,
  42. )
  43. ok = r.set(_key(ticket), body, ex=TICKET_TTL_SEC, nx=True)
  44. if not ok:
  45. raise RuntimeError("签发 WebView ticket 失败")
  46. return ticket
  47. return _redis_call(_issue, "Redis 不可用,无法签发 WebView ticket")
  48. def consume_ticket(ticket: str) -> Optional[dict]:
  49. raw = (ticket or "").strip()
  50. if not raw:
  51. return None
  52. def _consume(r) -> Optional[dict]:
  53. payload = _atomic_getdel(r, _key(raw))
  54. if not payload:
  55. return None
  56. if isinstance(payload, bytes):
  57. payload = payload.decode("utf-8")
  58. data = json.loads(payload)
  59. if not isinstance(data, dict):
  60. return None
  61. return data
  62. return _redis_call(_consume, "Redis 不可用,无法兑换 WebView ticket")