100 lines
2.8 KiB
Python
100 lines
2.8 KiB
Python
import hashlib
|
|
import hmac
|
|
import time
|
|
from uuid import uuid4
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
|
SERVICE_TIMESTAMP_HEADER = "X-ERP-QR-Timestamp"
|
|
SERVICE_NONCE_HEADER = "X-ERP-QR-Nonce"
|
|
SERVICE_SIGNATURE_HEADER = "X-ERP-QR-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
|