| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 |
- """web-admin 扫码登录会话:短码映射 + 状态机;优先 Redis,否则进程内存。"""
- from __future__ import annotations
- import json
- import secrets
- import time
- import uuid
- from typing import Any, Dict, Optional
- from app.core.config import settings
- _PREFIX = "wxqr:sess:"
- _SHORT_PREFIX = "wxqr:short:"
- _memory: Dict[str, Dict[str, Any]] = {}
- _short_map: Dict[str, str] = {}
- def _memory_clear_for_tests() -> None:
- _memory.clear()
- _short_map.clear()
- def _ttl() -> int:
- return int(getattr(settings, "WX_QR_SESSION_TTL_SEC", 180) or 180)
- def _redis():
- """复用 auth 侧 Redis 客户端模式;无 REDIS_HOST 返回 None。"""
- host = (settings.REDIS_HOST or "").strip()
- if not host:
- return None
- try:
- import redis
- return redis.Redis(
- host=host,
- port=int(settings.REDIS_PORT or 6379),
- db=int(settings.REDIS_DB or 0),
- password=settings.REDIS_PASSWORD or None,
- decode_responses=True,
- socket_timeout=2,
- )
- except Exception:
- return None
- def create_session(*, ttl_sec: Optional[int] = None) -> tuple:
- ttl = int(ttl_sec if ttl_sec is not None else _ttl())
- session_id = str(uuid.uuid4())
- short = secrets.token_urlsafe(16)[:32]
- now = int(time.time())
- data = {
- "sessionId": session_id,
- "shortCode": short,
- "status": "pending",
- "message": "",
- "createdAt": now,
- "expiresAt": now + ttl,
- "tokens": None,
- }
- r = _redis()
- if r:
- pipe = r.pipeline()
- pipe.setex(_PREFIX + session_id, ttl, json.dumps(data, ensure_ascii=False))
- pipe.setex(_SHORT_PREFIX + short, ttl, session_id)
- pipe.execute()
- else:
- _memory[session_id] = data
- _short_map[short] = session_id
- return session_id, short
- def resolve_session_id(short_or_id: str) -> Optional[str]:
- key = (short_or_id or "").strip()
- if not key:
- return None
- r = _redis()
- if r:
- sid = r.get(_SHORT_PREFIX + key)
- if sid:
- return sid
- if r.exists(_PREFIX + key):
- return key
- return None
- if key in _short_map:
- return _short_map[key]
- if key in _memory:
- return key
- return None
- def get_session(session_id: str) -> Optional[Dict[str, Any]]:
- sid = (session_id or "").strip()
- if not sid:
- return None
- r = _redis()
- if r:
- raw = r.get(_PREFIX + sid)
- if not raw:
- return None
- data = json.loads(raw)
- else:
- data = _memory.get(sid)
- if not data:
- return None
- data = dict(data)
- if int(time.time()) >= int(data.get("expiresAt") or 0):
- data["status"] = "expired"
- return data
- return data
- def _save(data: Dict[str, Any]) -> None:
- sid = data["sessionId"]
- ttl = max(1, int(data["expiresAt"]) - int(time.time()))
- r = _redis()
- if r:
- r.setex(_PREFIX + sid, ttl, json.dumps(data, ensure_ascii=False))
- else:
- _memory[sid] = data
- def mark_scanned(session_id: str) -> bool:
- data = get_session(session_id)
- if not data or data["status"] in ("expired", "success", "consumed", "failed"):
- return False
- if data["status"] == "pending":
- data["status"] = "scanned"
- _save(data)
- return True
- def mark_success(session_id: str, tokens: Dict[str, Any]) -> bool:
- data = get_session(session_id)
- if not data or data["status"] in ("expired", "consumed", "failed"):
- return False
- data["status"] = "success"
- data["tokens"] = tokens
- data["message"] = ""
- _save(data)
- return True
- def mark_failed(session_id: str, message: str) -> bool:
- data = get_session(session_id)
- if not data or data["status"] in ("expired", "consumed", "success"):
- return False
- data["status"] = "failed"
- data["message"] = message or "登录失败"
- data["tokens"] = None
- _save(data)
- return True
- def consume_success(session_id: str) -> Optional[Dict[str, Any]]:
- data = get_session(session_id)
- if not data or data["status"] != "success":
- return None
- tokens = data.get("tokens")
- data["status"] = "consumed"
- data["tokens"] = None
- _save(data)
- return tokens
- def invalidate(session_id: str) -> None:
- data = get_session(session_id)
- if not data:
- return
- data["status"] = "expired"
- data["tokens"] = None
- _save(data)
|