JhHardwareWRS_BackPoint/app/services/erp_login_security.py
2026-07-06 01:28:12 +08:00

100 lines
2.8 KiB
Python

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