WebHook
签名校验
使用 HMAC-SHA256 校验 WebHook 原始请求体,并通过时间窗口降低重放风险。
配置签名密钥后,Poco 会在每次投递中发送 X-Poco-Signature。生产接收端应同时校验签名和时间戳,再解析和处理 JSON。
签名算法
- 读取
X-Poco-Timestamp,记为timestamp。 - 读取 HTTP 原始请求体字节,记为
body_bytes。 - 拼接
timestamp.encode("utf-8") + b"." + body_bytes。 - 以 WebHook 密钥执行 HMAC-SHA256。
- 将十六进制摘要加上
sha256=前缀。
公式:
signature = "sha256=" + hex(HMAC_SHA256(secret, timestamp + "." + raw_body))必须使用网络收到的原始请求体。先解析 JSON 再重新序列化会改变空格、转义或键顺序,导致验签失败。
Python 实现
import hashlib
import hmac
def build_signature(secret: str, timestamp: str, body_bytes: bytes) -> str:
signing_payload = timestamp.encode("utf-8") + b"." + body_bytes
digest = hmac.new(
secret.encode("utf-8"),
signing_payload,
hashlib.sha256,
).hexdigest()
return f"sha256={digest}"
def verify_signature(
secret: str,
timestamp: str,
body_bytes: bytes,
signature_from_header: str,
) -> bool:
expected = build_signature(secret, timestamp, body_bytes)
return hmac.compare_digest(expected, signature_from_header or "")使用 hmac.compare_digest,不要用普通字符串 == 比较签名。
校验时间戳
签名本身不能阻止攻击者重放一份曾经合法的请求。接收端应拒绝与当前时间相差过大的 X-Poco-Timestamp,例如只接受 5 分钟内的请求:
from datetime import datetime, timedelta, timezone
def is_timestamp_valid(timestamp: str, max_skew_minutes: int = 5) -> bool:
try:
value = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
except ValueError:
return False
now = datetime.now(timezone.utc)
delta = abs(now - value.astimezone(timezone.utc))
return delta <= timedelta(minutes=max_skew_minutes)服务器时钟必须同步。时间窗口太小可能让网络延迟或时钟偏差造成误拒绝;5 分钟是常见起点,最终按部署环境决定。
推荐校验顺序
- 检查
X-Poco-Event、X-Poco-Event-Id、X-Poco-Timestamp和X-Poco-Signature是否存在。 - 读取原始请求体字节。
- 校验时间戳窗口。
- 计算签名并使用恒定时间比较。
- 解析 JSON。
- 检查请求头与请求体中的事件类型、事件 ID 是否一致。
- 使用事件 ID 做幂等判断。
- 把事件写入可靠队列并返回 2xx。
密钥管理
- 使用足够长的随机密钥,不要使用账号、项目名等可猜测文本。
- Poco 配置响应只返回
has_secret,不会返回原密钥;创建后应由双方安全保存。 - 不要在日志中记录签名密钥、完整请求头或敏感载荷。
- 轮换密钥时需要协调发送端和接收端,避免新旧密钥切换窗口内大量验签失败。
未配置密钥时 Poco 不发送 X-Poco-Signature。生产环境不建议关闭签名。