Skip to content

feat(alipay): 支付宝当面付直连插件(对标 #12.2 支付宝侧) - #5

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

worable233 merged 1 commit into
mainfrom
feat/alipay-direct

Conversation

@worable233

Copy link
Copy Markdown
Owner

对标 #12.2 的支付宝半边。审计指出支付只有第三方聚合(epay)一条路,缺官方直连。

做了什么

新插件 packages/plugins/alipay:alipay.trade.precreate 扫码下单 + 异步通知验签 + 关单,
商户自有密钥,不经中间商;配套后台配置页(AppID / 网关 / 应用私钥 / 支付宝公钥,四语言)。

没有商户号,怎么验

密码学与协议不需要商户凭据也能验透:自签一对 RSA 密钥分别扮演「商户」和「支付宝」,
签名/验签/回调/金额换算全部跑真实 crypto —— 没有一处 mock 掉签名。

三个必须按支付宝原文来、否则必然出错的点:

点 错误做法 后果
待签名串排序 localeCompare 大小写混排时顺序与支付宝不一致
空值处理 保留空参数 所有请求验签失败
验签对象 JSON.stringify(parsed) 字段顺序/空格/转义不同 → 「签名明明对却验不过」

设计取舍

  • 非结算通知也回 success(如 TRADE_CLOSED):回 4xx 会让支付宝无限重推,而这些状态
    本来就不该结算,重推多少次结果一样。
  • 业务错误(code != 10000)标 definitive:参数/商户配置问题重试无意义。
  • 未配置密钥时 getMethods() 返回空:不制造「能点但必失败」的支付按钮。
  • 网关只允许 https 域名:允许自定义是为了沙箱,但可配置的出站地址天然是 SSRF 入口,
    所以拒 IP 字面量、localhost、.local/.internal。
  • 私钥不回显:后台只显示「已配置」。
  • 关单失败不假装成功:返回 false,让内核保留记录等过期扫描。

验证

  • pnpm test 33 例:协议 23(签名往返、篡改、错密钥、裸 base64 私钥补 PEM、分↔元、
    响应原文验签、嵌套节点提取、业务错误、回调四类拒绝、网关 URL 校验)+ 插件接线 10
    (未配置不给支付方式、CNY 之外不出网、precreate 请求含签名、回调结算/不结算、
    未签名通知在结算前被拒、配置持久化、坏密钥拒绝保存)
  • pnpm typecheck / pnpm build 通过;install-into-kernel.mjs 成功装入内核 data/plugins

未完成

  • 未用真实商户号跑真单(需 AppID + 应用私钥 + 支付宝公钥 + 可公网回调的 https 域名)
  • 微信支付 v3 未做(APIv3 密钥 + 平台证书 + 回调 AES-GCM 解密,单独一件)

因此按 TRACK §四 的约定,本 PR 只能声称「驱动已实现,未联调」,不能写「已支持支付宝」。

审计指出支付只有第三方聚合(易支付)一条路,缺「直连官方渠道」。这是支付宝半边:
当面付(`alipay.trade.precreate`)扫码下单 + 异步通知验签 + 关单,商户自有密钥,
不经中间商。

## 能被验证的部分(在没有商户号的情况下)

密码学与协议全部跑真实实现,没有一处 mock 掉签名:

- RSA2(SHA256withRSA)签名/验签、裸 base64 私钥自动补 PEM 头(控制台复制出来就是裸的)
- 待签名串按 **ASCII 字节序**排序并排除空值 —— 用 `localeCompare` 会在大小写混排时
  与支付宝不一致;保留空值则所有请求都验签失败
- 响应验签取**节点原文**(不是 `JSON.stringify(parsed)`):支付宝签的是响应体里那段
  文本,重新序列化会在字段顺序/空格/转义上不一致,症状是「明明签名对却验不过」
- 回调:签名 → app_id 必须匹配 → 只有 `TRADE_SUCCESS`/`TRADE_FINISHED` 才结算
- 金额:分 ↔ 元字符串,回调用整数换算(浮点加法会把 0.1+0.2 变成 0.30000000000000004)
- 时间戳必须是北京时间 `YYYY-MM-DD HH:mm:ss`,ISO 8601 会被直接拒

## 设计取舍

- **未付款/已关闭的通知也回 `success`**:回 4xx 会让支付宝一直重推,而这些状态本来
  就不该结算,重推多少次结果一样。
- **业务错误(code != 10000)标记为 definitive**:参数/商户配置问题重试一百次也一样。
- **网关地址只允许 https 域名**:允许自定义是为了沙箱,但「可配置的出站地址」天然是
  SSRF 入口,所以拒 IP 字面量、localhost、.local/.internal。
- **私钥不回显**:后台只显示「已配置」,不把密钥材料吐回浏览器。
- **关单失败不假装成功**:返回 false,让内核保留记录等过期扫描。

## 配套

后台配置页(`frontend/`):AppID / 网关 / 应用私钥 / 支付宝公钥,四语言目录。
装了插件才有支付方式 —— 没配密钥时 `getMethods()` 返回空,不制造「能点但必失败」的支付按钮。

## 验证

`pnpm test` **33 例**:协议 23 例(签名往返、篡改、错密钥、裸私钥、金额换算、响应原文
验签、业务错误、嵌套节点提取、回调四类拒绝、网关 URL 校验)+ 插件接线 10 例
(未配置不给支付方式、CNY 之外不出网、precreate 请求含签名、回调结算与不结算、未签名通知
在结算前被拒、配置持久化与坏密钥拒绝保存)。
`pnpm typecheck` / `pnpm build` 通过;`install-into-kernel.mjs` 成功装入内核 data/plugins。

## 未完成(写清楚,别当成已经能用)

- **未用真实商户号跑过真单**:需要 AppID + 应用私钥 + 支付宝公钥 + 可公网回调的 https 域名。
- **微信支付 v3 未做**(需要 APIv3 密钥 + 平台证书,且回调要 AES-GCM 解密,单独一件)。
@worable233
worable233 merged commit 7c33e29 into main Oct 25, 2026
1 check passed
@worable233
worable233 deleted the feat/alipay-direct branch October 25, 2026 04:47
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