Repository navigation
Conversation
StringKit#appendUrlQuery 负责拼装发给商户的异步通知/同步跳转地址
(PayMchNotifyService#createNotifyUrl / createReturnUrl), 以及收银台跳转地址
(ChannelUserIdController, MchTransferController)。存在两个独立缺陷:
1) 用 encodeQuery 编码单个参数值
URLUtil.encodeQuery() 的语义是编码整条 query string, 会刻意保留 & = + ? / 等
分隔符, 用在单个参数值上会导致:
subject="Salt & Pepper Shaker" -> subject=Salt%20&%20Pepper%20Shaker
(一个参数被拆成两个, 商户收到 subject="Salt ")
extParam="a=b" -> extParam=a=b (取值被截断)
sign="ab+cd==" -> sign=ab+cd== (服务端按 form 解码成 "ab cd==")
subject/body 只有 @notblank 约束, 上述取值完全合法, 缺陷可被真实触发。
修复: 改用 URLUtil.encodeAll()。
2) query 被拼到了 fragment 之后
原实现直接把参数追加到整个 url 末尾。当回调地址带 fragment 时
(如 SPA 的 https://mch.example.com/cb#/result), 生成的是
https://mch.example.com/cb#/result?payOrderId=P123
? 之后的内容属于 fragment, 不会发给服务端, 商户永远收不到参数。
讽刺的是 appendUrlQueryWithHutool 上的 @deprecated 注释写的正是
"当回调地址是 http://abc.com/#/abc 时存在位置问题", 但替代实现同样有这个问题。
修复: 先切出 fragment, query 拼在 base 上, 最后再把 fragment 接回去。
签名不受影响: 签名是对原始(未编码)参数计算的, 接收方解码后得到同样的原始值。
回归测试: 新增 StringKitAppendUrlQueryTest (7 个用例), 修复前分别有 2 个
(编码) 和 2 个 (fragment) 用例失败, 修复后全部通过。
rwzdk
force-pushed
the
fix/appendurlquery-value-encoding
branch
from
October 27, 2026 16:57
c22b72d to
974c7b7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
现象
商户配置的异步通知地址 / 同步跳转地址,在特定订单内容或特定回调地址形态下会被拼成错误的 URL。两个独立缺陷,都在同一个函数里。
StringKit#appendUrlQuery是拼接这类地址的唯一入口:PayMchNotifyService#createNotifyUrl(支付 / 退款 / 转账通知)PayMchNotifyService#createReturnUrl(支付完成同步跳转,ChannelNoticeController、QrCashierController调用)ChannelUserIdController#channelUserIdCallback、MchTransferController(收银台跳转)缺陷一:用
encodeQuery编码单个参数值jeepay-core/src/main/java/com/jeequan/jeepay/core/utils/StringKit.java:65URLUtil.encodeQuery()的语义是编码整条 query string,因此它刻意保留 query 语法里的分隔符
& = + ? /。这里却把它用在单个参数值上:&=+application/x-www-form-urlencoded解码成空格实测(Hutool 5.8.26):
这些取值完全合法:
UnifiedOrderRQ#subject/#body只有@NotBlank约束。商户正常填写"Salt & Pepper Shaker" 这样的商品标题,通知就会错乱。
修复:改用
URLUtil.encodeAll()(编码全部保留字符,语义正确对应"单个参数值")。缺陷二:query 被拼到了 fragment 之后
原实现把参数直接追加到整个 url 末尾。当回调地址带 fragment 时:
?之后的内容属于 fragment,根本不会发给服务端,商户永远收不到参数。值得注意的是,旁边
appendUrlQueryWithHutool上的@Deprecated注释写的正是:也就是说这个位置问题当初就是为了修它才废弃 Hutool 版本的 —— 但替代实现同样有这个问题。
对照实测:
SPA 应用用 hash 路由做支付回跳页(
https://mch.example.com/#/payResult)非常常见。修复:先切出 fragment,query 拼在 base 上,最后把 fragment 接回去。
签名兼容性
不受影响。
JeepayKit#getSign是对未编码的原始参数计算的(k + "=" + value),接收方把 query 解码后拿到的仍是同一个原始值,因此签名校验结果不变。
测试
新增
jeepay-core/src/test/java/com/jeequan/jeepay/core/utils/StringKitAppendUrlQueryTest.java,7 个用例,按标准 query string 规则反解生成的 URL 并断言能原样还原:
&时不得被拆成两个参数;=时不得截断;+时不得被服务端解码成空格;null/ 空 url / 空参数原样返回;#之前且 fragment 原样保留;缺陷一 修复前(真实输出)
缺陷二 修复前(单独回退 fragment 逻辑,保留编码修复)
修复后(真实输出,
jeepay-core全量)执行的命令:
未执行的部分(如实说明):没有启动完整应用做端到端通知投递验证(需要 MySQL / Redis / MQ,
本地不具备)。验证停留在单元层面:证明了生成的 URL 能被标准规则原样反解、且 fragment 位置正确。
Maven 全程以
-o(离线)运行,未下载新的第三方依赖。备注
JeepayKit#genUrlParams(渠道请求参数),本 PR 修的是StringKit#appendUrlQuery(发给商户的回调地址),是两处独立的编码缺陷。对绝大多数存量商户无行为变化。
Prepared with an AI coding agent.