Skip to content

feat(wechatpay): 微信支付 v3 Native 直连插件(对标 #12.2 微信侧) - #6

Merged
worable233 merged 1 commit into
mainfrom
feat/wechatpay-direct
Oct 25, 2026
Merged

worable233 merged 1 commit into
mainfrom
feat/wechatpay-direct

Conversation

@worable233

Copy link
Copy Markdown
Owner

对标 #12.2 的微信半边(支付宝半边已在 #5 合入)。

与支付宝那半边不是「照着抄」

支付宝 微信 v3
签名位置 表单里的 sign Authorization: WECHATPAY2-SHA256-RSA2048 ... 头
应答 表单文本,验签取节点原文 JSON + 响应头签名
回调 表单 + 签名串 AES-256-GCM 密文,用 APIv3 密钥解

三个必然踩到的坑

  1. 请求签名串 METHOD\nURL\nTIMESTAMP\nNONCE\nBODY\n —— 末尾换行属于签名内容,
    少一个就是 401 SIGN_ERROR,且错误码不说是哪段错了。
  2. 应答/回调签名串只有三段 TIMESTAMP\nNONCE\nBODY\n,且必须用原始字节。
    所以回调用 kind: 'raw' 路由拿未解析流:内核 JSON 解析会重新序列化,
    字段顺序/空格/转义一变,签名永远验不过。
  3. ciphertext 是 base64 的 密文 || 16B 认证标签,整段喂给 decipher 会在 final() 抛错。

设计取舍

  • 应答无签名即拒绝:code_url 决定用户把钱扫给谁,伪造应答等于劫持订单
  • 回调时间戳 5 分钟容差:重推必须带新签名,旧报文原样重放不能通过
  • 非 SUCCESS 也回 SUCCESS:CLOSED/PROCESSING 不该结算,但回 FAIL 会招致无限重推
  • 验签/解密失败回 401 + FAIL:报文不可信,必须让它重试
  • 密钥留空 = 保持不变:后台不回显密钥,改商户号不该逼管理员重贴三份密钥
  • API 基址只允许 https 域名:可配置是为了测试,那就得按 SSRF 入口防

验证

pnpm test 37 例:协议 24 + 接线 13,含「未签名应答被拒」「篡改回 401」「AppID 不符拒绝」
「密钥留空继承」「坏配置不落盘」。typecheck / build 通过,配置页(四语言)构建产物齐全,
install-into-kernel.mjs 成功装入内核 data/plugins。

未完成

  • 未用真实商户号跑真单(需 mchid + 证书序列号 + 私钥 + APIv3 密钥 + 平台公钥 + 公网回调地址)
  • 平台证书自动轮换未做(微信平台证书会轮换,生产上需定时拉 /v3/certificates 解密替换)

按 TRACK §四,本 PR 只能说「驱动已实现,未联调」。

补上 12.2 的微信半边。与支付宝那半边的结构差异决定了这不是「照着抄」:

- 支付宝把 `sign` 放在表单里;微信是 `Authorization: WECHATPAY2-SHA256-RSA2048 ...` 头;
- 支付宝的应答是表单文本;微信是 JSON + **响应头签名**,要用原始报文验;
- 支付宝回调用自己的签名串;微信回调是 **AES-256-GCM 密文**,要用 APIv3 密钥解。

## 三个必然踩到的坑

1. 请求签名串 `METHOD\nURL\nTIMESTAMP\nNONCE\nBODY\n` —— 末尾那个换行属于签名内容。
   少一个就是 401 `SIGN_ERROR`,而错误码不会告诉你是哪一段错了。
2. 应答/回调签名串只有三段 `TIMESTAMP\nNONCE\nBODY\n`,与请求的不同;且必须用**原始字节**。
   所以回调用 `kind: 'raw'` 路由拿未解析流 —— 内核 JSON 解析会重新序列化,字段顺序/
   空格/转义一变,签名就永远验不过(这不是偶发,是必然)。
3. `ciphertext` 是 base64 的 `密文 || 16 字节认证标签`,不是纯密文;整段喂给 decipher
   会在 `final()` 抛「unable to authenticate data」。

## 设计取舍

- **应答无签名即拒绝**:`code_url` 决定用户把钱扫给谁,伪造的应答等于劫持订单。
- **回调时间戳 5 分钟容差**:微信会重推,但重推必须带新签名;旧报文原样重放不能通过。
- **非 SUCCESS 状态也回 `{"code":"SUCCESS"}`**:`CLOSED`/`PROCESSING` 不该结算,
  但回 FAIL 会让微信一直重推。
- **验签/解密失败回 401 + `FAIL`**:这类报文不可信,必须让它重试。
- **AppID 用常量时间比较**。
- **密钥留空 = 保持不变**:后台不回显密钥材料,改个商户号不该逼管理员重贴三份密钥。
- **API 基址只允许 https 域名**(可配置是为了测试,那就必须按 SSRF 入口来防)。

## 验证

`pnpm test` **37 例**:协议 24(签名串形状、Authorization 头、裸 base64 私钥、应答验签、
篡改/错密钥/过期时间戳拒绝、AES-GCM 往返与篡改拒绝、错误密钥拒绝、金额单位是分、
description 截断 127、错误体解析、配置校验、SSRF 校验、常量时间 AppID 比较)
+ 接线 13(未配置不给支付方式、下单并验应答签名、**未签名应答被拒**、CNY 之外不出网、
原始报文回调结算、非 SUCCESS 不结算、篡改回 401 FAIL、AppID 不符拒绝、金额缺失拒绝、
密钥留空继承、坏配置不落盘)。

`pnpm typecheck` / `pnpm build` 通过;`frontend/` 配置页(四语言)构建出
`frontend/dist/index.js`;`install-into-kernel.mjs` 成功装入内核 `data/plugins`。

## 未完成

- **未用真实商户号跑过真单**:需 mchid + 商户 API 证书序列号 + 私钥 + APIv3 密钥 +
  平台公钥(或平台证书)+ 可公网回调的 https 域名。
- **平台证书自动轮换未做**:现在直接配平台公钥/证书内容;微信平台证书会轮换,
  生产上还需要定时下载 `/v3/certificates`(用 APIv3 密钥解密)并替换。
@worable233
worable233 merged commit 806b243 into main Oct 25, 2026
1 check passed
@worable233
worable233 deleted the feat/wechatpay-direct branch October 25, 2026 05:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant