Skip to content

fix(alipay): 加固授权回调 state 解析 + 异步通知补业务字段交叉校验 - #106

Merged
jeequan merged 1 commit into
devfrom
fix/alipay-callback-bizcheck
May 27, 2026
Merged

fix(alipay): 加固授权回调 state 解析 + 异步通知补业务字段交叉校验#106
jeequan merged 1 commit into
devfrom
fix/alipay-callback-bizcheck

Conversation

@jeequan

@jeequan jeequan commented May 24, 2026

Copy link
Copy Markdown
Owner

背景

修复 #81(感谢 @xiaowuDev 提交的 PR 描述定位到这两处问题,原 PR 关闭后由本 PR 重新实现)。

修复 1:授权回调 state 参数解析鲁棒性

原问题AlipayBizControllerredirectAppToAppAuthappToAppAuthCallback 都直接 isvAndMchAppId.split("_")[0]/[1],未做长度 / 空值 / 空串校验。异常或被构造的 state(如 `/redirectAppToAppAuth/badid` 或 state 为空字符串)会触发 ArrayIndexOutOfBoundsException 或空指针,回调链路出现 500。

修复

  • 抽出 AlipayKit.parseIsvAndMchAppIdState(String) 工具方法:用 indexOf 而非 split(避免 mchAppId 内部含 _ 时被切碎),任一段为空返回 null
  • 两个回调入口改用工具方法 + null 校验 + mchAppService.getById 结果空校验,抛 BizException 由现有异常处理统一展示
  • 顺手清理 redirectAppToAppAuth 里两行无效代码(getSandbox() 返回值未使用、isvNo 局部变量从未读取)

修复 2:异步通知业务字段交叉校验

原问题AlipayChannelNoticeService.doNotice 验签通过后直接信任 jsonParams,仅取了 trade_no / buyer_id / trade_status 来设置 ChannelRetMsg。验签通过仅证明请求来自支付宝并由本商户密钥签名,不保证回调内容就是这笔本地订单。多商户配置切换、跨订单签名穿越、沙箱与生产串扰等场景下,仍可能出现 out_trade_no / 金额 / app_id 与本地订单上下文不一致的回调,按现有逻辑会把这笔"看似合法"的回调直接当成功回写订单,造成错单和账务异常。

修复:在验签通过后、设置 ChannelRetMsg 前补一道校验:

  • out_trade_no 必须等于 payOrder.payOrderId
  • app_id 必须等于商户配置里的 appId(ISV/普通商户分别取对应配置)
  • total_amount(元)必须等于本地金额(分→元,用 BigDecimal.compareTo 比对,避免 scale 差异)

任一字段不一致:

  • 记 ERROR 日志,带具体不匹配的字段与双方值,方便运维排查
  • 返回 SUCCESS 给支付宝,阻断 8 次重试浪费资源
  • ChannelState 设为 UNKNOWN(而非 CONFIRM_FAIL,避免被下游误判为"确认失败"触发退款补偿逻辑)
  • 订单状态不更新,保持原状

不做的事

  • 不校验 seller_idAlipayNormalMchParams 当前没存 PID 字段,强行做要么改 schema 要么改 channelExtra,破坏面太大,留到未来需要时再补。app_id 校验已能拦截 PR 优化支付宝回调参数解析,增加参数格式校验 #81 描述的主要场景。
  • 不补单元测试:与项目惯例一致,jeepay-payment/src/test 是空目录占位状态;校验逻辑组织成纯静态方法 / 局部变量,便于未来引入测试基础设施时补。

验证

  • mvn -pl jeepay-payment -am compile 通过
  • 构造测试场景验证:state=空 / state=`badid` / state=`isv_` / state=`_mch` 都被拒绝返回 4xx 而非 500
  • 构造测试场景验证:异步回调 out_trade_no 与本地订单不一致 / total_amount 不一致 / app_id 不一致 时返回 SUCCESS 但订单状态不变,日志能看到 ERROR 行

发布节奏

下个补丁版(V3.2.9)一起发出。本 PR 不改 version.md,由 V3.2.9 发版本 PR 集中撞版本号。

@jeequan
jeequan force-pushed the fix/alipay-callback-bizcheck branch from 06618c8 to cdb9421 Compare May 27, 2026 07:51
修复 #81(感谢 @xiaowuDev 提交的 PR 描述定位到这两处问题)。

1) 授权回调 state 参数解析鲁棒性
- 原 AlipayBizController.redirectAppToAppAuth / appToAppAuthCallback 直接
  split("_")[0]/[1],没做长度、空值、空串校验,异常或被构造的 state
  会触发 ArrayIndexOutOfBoundsException / 空指针,回调链路出现 500。
- 抽出 AlipayKit.parseIsvAndMchAppIdState(String) 工具方法:用 indexOf 而非
  split(避免 mchAppId 内部含 "_" 时被切碎),任一段为空都返回 null。
- 两个回调入口改用工具方法 + null 校验 + mchAppService.getById 结果空校验,
  抛出 BizException 由现有异常处理统一展示给用户。
- 顺手清理 redirectAppToAppAuth 里两行无效代码(getSandbox() 返回值未使用、
  isvNo 局部变量从未读取)。

2) 异步通知业务字段交叉校验
- 原 AlipayChannelNoticeService.doNotice 验签通过后直接信任 jsonParams,
  仅取了 trade_no / buyer_id / trade_status 来设置 ChannelRetMsg。
  验签通过仅证明请求来自支付宝并由本商户密钥签名,不保证回调内容就是
  这笔本地订单。多商户配置切换 / 跨订单签名穿越 / 沙箱与生产串扰
  等场景下,仍可能出现 out_trade_no / 金额 / app_id 与本地订单上下文
  不一致的回调,按现有逻辑会把这笔"看似合法"的回调直接当成功来回写订单。
- 在验签通过后、设置 ChannelRetMsg 前补一道校验:
  out_trade_no 必须等于 payOrder.payOrderId、
  app_id 必须等于商户配置里的 appId、
  total_amount(元)必须等于本地金额(分→元,用 BigDecimal compareTo
  比对避免 scale 差异)。
- 任一字段不一致:记 ERROR 日志(带具体不匹配的字段与双方值,便于排查)+
  返回 SUCCESS 给支付宝阻断重试 8 次浪费资源 + 不更新订单状态。
- ChannelState 用 UNKNOWN 而非 CONFIRM_FAIL,避免被下游误判为"已确认失败"
  从而触发退款补偿逻辑。
@jeequan
jeequan force-pushed the fix/alipay-callback-bizcheck branch from cdb9421 to b20b680 Compare May 27, 2026 07:53
@jeequan
jeequan merged commit 6259ae1 into dev May 27, 2026
2 checks passed
@jeequan
jeequan deleted the fix/alipay-callback-bizcheck branch May 27, 2026 07:53
pull Bot pushed a commit to w346489584/jeepay that referenced this pull request May 27, 2026
按 upgrade.md 风格惯例(参考 V3.2.7 / V3.2.8 段),把 V3.2.9 段从展开版
(带 ">" 引言段 + 每条 3-5 行类名 / 机制 / 详细技术解释)改成每条 1-2 行
的简短风格。

upgrade.md 是面向用户的版本说明,关心"哪些场景受影响 / 是否需要升级",
详细机制保留在 PR jeequan#102 / jeequan#106 描述里。
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