"""穿山甲激励视频"服务端发奖回调"的验签。 发奖闭环:激励视频播完 → 穿山甲服务器 GET 回调本服务(带 user_id / trans_id / sign 等) → 本服务**验签**确认回调确实来自穿山甲(而非伪造请求) → 幂等发金币。 客户端不参与发奖,被破解也刷不到钱。 ⚠️ 关于验签方案:穿山甲后台的"奖励校验"实际签名算法以控制台为准(不同接入方式可能是 HMAC 共享密钥,也可能是穿山甲 RSA 公钥验签)。这里用 **HMAC-SHA256(排序参数, 密钥)** 作为 默认实现并配套自验签测试。真实接入时**只需改本文件的 `verify_callback_sign` / `build_sign` 这一处**——发奖、幂等、每日限额逻辑(crud/ad_reward.py)与验签方案完全无关,不受影响。 """ from __future__ import annotations import hashlib import hmac # 验签时排除的参数(sign 自身不参与签名;空值参数也不计入) _SIGN_EXCLUDE = {"sign"} def _canonical(params: dict[str, str]) -> str: """把参与签名的参数按 key 升序拼成 `k=v&k=v`。空值剔除,保证两端构造一致。""" items = sorted( (k, v) for k, v in params.items() if k not in _SIGN_EXCLUDE and v not in (None, "") ) return "&".join(f"{k}={v}" for k, v in items) def build_sign(params: dict[str, str], secret: str) -> str: """对参数计算 HMAC-SHA256 十六进制签名(测试 / 自验用)。""" msg = _canonical(params).encode("utf-8") return hmac.new(secret.encode("utf-8"), msg, hashlib.sha256).hexdigest() def verify_callback_sign(params: dict[str, str], secret: str) -> bool: """校验回调签名。`params` 是回调全部 query 参数(含 sign)。密钥为空一律判失败。""" if not secret: return False got = params.get("sign") or "" expect = build_sign(params, secret) # 定长比较防时序侧信道 return hmac.compare_digest(got, expect)