微信消息回调:签名校验、XML 解析与事件处理
如果说 /send 是“你主动发起推送”的入口,那微信回调就是“外部世界把事件推给你”的入口。
这两者的差别很大:
- 你调用
/send时,参数是否正确由你控制。 - 微信回调时,你拿到的是 XML、事件字段不稳定、回调可能重复、而且还要求你先通过签名验证。
这一篇写的是:怎么把这条“不稳定输入链路”变成系统里最可靠的一环。
实现入口在:
node-functions/routes/wechat-msg.ts
先把流程讲清楚:两阶段 + 一条事件主线
微信回调可以粗暴理解成两阶段:
- GET 验证阶段:你证明“你是你”。
- POST 消息阶段:微信把消息/事件推给你。
而项目里最关键的一条事件主线是“绑定”:
flowchart TD
A[配置服务器 URL] --> B["GET /wechat 验证签名"]
B -->|"ok"| C[微信开始推事件]
C --> D["POST /wechat/:channelId"]
D --> E[解析 XML]
E --> F{事件类型?}
F -->|"subscribe/SCAN"| G["提取 scene/bindcode"]
G --> H[绑定码校验 + 幂等写入 OpenID]
H --> I[返回 success]
F -->|"其它消息"| J["按需处理/忽略"]
GET:签名验证不是形式主义
微信会要求你实现一个 GET 接口,用于“服务器配置验证”。
校验思路是固定的:
- 取
token、timestamp、nonce - 排序拼接
- 做
sha1 - 比较
signature
项目这里做了一个兼容取舍:
/wechat:无channelId,兼容旧配置,token 为空/wechat/:channelId:从渠道配置读取 token
这个兼容在“迁移已有用户”时很有用,但它也带来一个现实提醒:
- token 是安全边界的一部分。
- token 为空意味着“这条入口几乎没有签名保护”,只适合过渡,不适合长期使用。
POST:微信回调的真实难点在 XML 与幂等
1) XML 解析
微信回调是 XML(而不是 JSON),所以需要 xmlBody 中间件把 body 解析成可用结构。
这里建议遵守两个原则:
- 解析失败要尽早失败:不要让业务层在
undefined上做推断。 - 字段缺失要容错:不同事件携带字段差异很大。
2) 事件分发
最常见、也最有价值的事件是:
subscribe:用户关注SCAN:已关注用户扫码
它们的共同点:都可能携带“场景值”(scene),可以作为 BindCode 的载体。
3) 绑定链路与幂等
绑定是典型的“事件驱动写入”,而事件驱动最怕两件事:
- 重复回调(网络重试、平台重放)
- 乱序回调(subscribe 与 scan 的先后不确定)
因此绑定逻辑必须是幂等的:
- 同一个
appId + openId写入多次,最终状态不变 - 同一个 bindcode 被消费多次,只能第一次成功
实现层面通常靠两类索引达成:
OPENID_INDEX(appId, openId):保证“同一用户不会重复绑定”- bindcode 的
status:保证“同一绑定码不会被重复消费”
一张时序图:从扫码到写入 OpenID
sequenceDiagram
autonumber
participant W as WeChat Server
participant R as /wechat Route
participant B as bindcodeService
participant O as openidService
participant K as KV (via Edge)
W->>R: POST /wechat/:channelId (XML)
R->>R: XML parse
R->>R: event dispatch
R->>B: verify/consume(bindcode)
B->>K: get/put bindcode status
K-->>B: ok
R->>O: upsert(openId for app)
O->>K: put OPENID + INDEX
K-->>O: ok
R-->>W: success
常见坑与排障(非常“真实”的部分)
-
签名一直验证失败:
- 先确认
token是否真的与微信后台配置一致 - 再确认你验证的是同一个 URL(带不带
channelId)
- 先确认
-
回调收不到 / 收到但解析失败:
- 很多时候不是业务 bug,是 XML body 没被正确解析(中间件顺序、Content-Type)
-
绑定偶发失败:
- 优先怀疑“幂等没做好”(重复回调导致状态竞争)
- bindcode 是否过期/已消费
-
绑定成功但推送发不出去:
- 绑定链路 ok 不代表 token ok,token 状态需要单独观测
启发:事件驱动系统的基本功,就是把“不确定”收敛成“可重复执行”
- 外部平台会重试、会重复、会乱序,这是常态。
- 你的系统要做的不是“假设它不会发生”,而是让写入逻辑天然幂等。
- 让每个关键状态(bindcode consumed、openid index、token status)都能被观测到,才有排障能力。
