Files
ouzhou 04edb50acc feat(admin/ad): 提现自动对账后台开关 + 客户端广告 flags 端点 + 穿山甲多 m-key 验签 (#59)
- 自动对账接入 app_config 运行时配置(新增 bool 配置类型):env 部署总闸 + DB 运营日常开关
  双层;worker 每轮读 DB 即时生效,健康检查改报「env AND DB」实际生效态
- 新增 GET /api/v1/platform/flags(不鉴权)下发 comparing_ad_enabled 远程 kill-switch,
  客户端拉取后缓存;空库回退默认 True
- 穿山甲发奖回调支持多激励位 m-key(三命名项 PANGLE_REWARD_SECRET_TEST/_DEDICATED/_PROD
  + 旧逗号分隔合并去重),verify_callback_sign_any 逐个验签任一通过即受理;向后兼容旧单 key
- 补 test_platform / bool config 用例;app_config 文档同步

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: OuYingJun1024 <1034284404@qq.com>
Reviewed-on: #59
Co-authored-by: ouzhou <ouzhou@wonderable.ai>
Co-committed-by: ouzhou <ouzhou@wonderable.ai>
2026-06-17 10:37:54 +08:00

55 lines
2.6 KiB
Python

"""穿山甲激励视频"服务端发奖回调"的验签。
发奖闭环:激励视频播完 → 穿山甲服务器 GET 回调本服务(带 user_id / trans_id / sign 等)
→ 本服务**验签**确认回调确实来自穿山甲(而非伪造请求) → 幂等发金币。
客户端不参与发奖,被破解也刷不到钱。
验签方案(穿山甲 GroMore「广告位 → 服务端激励回调」官方规范,见 supportcenter/26240。
我们客户端是 useMediation(true) 融合,回调走 GroMore 广告位层级,**不是**联盟代码位层级):
**sign = SHA256("{m-key}:{trans_id}")** 的十六进制串。注意:
- 是**普通 SHA256**,不是 HMAC,也不是 RSA;
- **只对 `m-key:trans_id` 这一个字符串**签名,user_id / ecpm / extra 等其余参数**不参与**签名
(它们的可信度由"能算出正确 sign = 知道 m-key"间接保证——伪造者没有 m-key 就连合法 sign
都造不出,自然也无法注入假 user_id/ecpm/extra)。
m-key(安全密钥)在穿山甲后台「GroMore 聚合管理 → 搜广告位ID → 编辑」处获取,配到
`settings.PANGLE_REWARD_SECRET`。(联盟代码位层级用的是另一套 Security Key + isValid 响应,我们不走那条。)
"""
from __future__ import annotations
import hashlib
import hmac
def build_sign(trans_id: str, secret: str) -> str:
"""按穿山甲规范计算 sign = SHA256("{SecurityKey}:{trans_id}") 的十六进制串。
secret 即后台的 Security Key。自验签测试 / 模拟回调脚本也用这一个函数,保证两端一致。
"""
msg = f"{secret}:{trans_id}".encode("utf-8")
return hashlib.sha256(msg).hexdigest()
def verify_callback_sign(params: dict[str, str], secret: str) -> bool:
"""校验回调签名。`params` 是回调全部 query 参数(含 sign / trans_id)。
密钥为空或缺 trans_id 一律判失败。比较用定长比较防时序侧信道。
"""
if not secret:
return False
trans_id = params.get("trans_id") or ""
if not trans_id:
return False
got = params.get("sign") or ""
expect = build_sign(trans_id, secret)
return hmac.compare_digest(got, expect)
def verify_callback_sign_any(params: dict[str, str], secrets: list[str]) -> bool:
"""对**多个** m-key 逐个验签,任一通过即接受。
多广告位场景:每个 GroMore 广告位的 m-key 由后台各自生成、互不相同,但本服务用
同一个回调 URL 接所有位的回调。配置里放多个密钥(逗号分隔),回调到达时挨个试。
仍然安全:伪造者必须知道其中某个 m-key 才能算出合法 sign;空列表 → 一律失败。
"""
return any(verify_callback_sign(params, s) for s in secrets if s)