import hashlib import hmac import time from uuid import uuid4 from fastapi import HTTPException SERVICE_TIMESTAMP_HEADER = "x-service-timestamp" SERVICE_NONCE_HEADER = "x-service-nonce" SERVICE_SIGNATURE_HEADER = "x-service-signature" _seen_nonces: dict[str, int] = {} def build_service_signature( method: str, path: str, timestamp: str, nonce: str, body: bytes, secret: str, ) -> str: message = "\n".join([ method.upper(), path, timestamp, nonce, body.decode("utf-8"), ]) return hmac.new(secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest() def signed_headers(method: str, path: str, body: bytes, secret: str) -> dict[str, str]: timestamp = str(int(time.time())) nonce = uuid4().hex signature = build_service_signature(method, path, timestamp, nonce, body, secret) return { SERVICE_TIMESTAMP_HEADER: timestamp, SERVICE_NONCE_HEADER: nonce, SERVICE_SIGNATURE_HEADER: signature, } def _cleanup_seen_nonces(now_ts: int, ttl_seconds: int) -> None: expired = [ nonce for nonce, seen_at in _seen_nonces.items() if now_ts - seen_at > ttl_seconds ] for nonce in expired: _seen_nonces.pop(nonce, None) def verify_service_signature( method: str, path: str, timestamp: str | None, nonce: str | None, body: bytes, signature: str | None, secret: str, ttl_seconds: int = 300, ) -> None: if not secret: raise HTTPException(status_code=500, detail="ERP扫码登录服务密钥未配置") try: timestamp_value = int(str(timestamp)) except (TypeError, ValueError) as exc: raise HTTPException(status_code=401, detail="ERP服务签名无效") from exc now_ts = int(time.time()) if abs(now_ts - timestamp_value) > ttl_seconds: raise HTTPException(status_code=401, detail="ERP服务签名已过期") _cleanup_seen_nonces(now_ts, ttl_seconds) nonce_value = str(nonce or "") if not nonce_value or nonce_value in _seen_nonces: raise HTTPException(status_code=401, detail="ERP服务签名无效") try: expected = build_service_signature( method, path, str(timestamp), nonce_value, body, secret, ) except UnicodeDecodeError as exc: raise HTTPException(status_code=401, detail="ERP服务签名无效") from exc if not hmac.compare_digest(expected, str(signature or "")): raise HTTPException(status_code=401, detail="ERP服务签名无效") # This in-process nonce store only protects one running process. Production # multi-worker deployments should move replay tracking to shared storage. _seen_nonces[nonce_value] = now_ts