外部 App 内嵌 H5 如何唤起支付宝支付
结论
这两种场景适合采用不同的支付方案:
- 外部 App 原生页面直接发起支付:使用支付宝的 App 支付产品。服务端调用
alipay.trade.app.pay生成签名后的订单字符串,再由 App 通过支付宝客户端 SDK 发起支付。 - 外部 App 内嵌 H5 发起支付:使用手机网站支付产品。服务端调用
alipay.trade.wap.pay生成支付表单或跳转地址,H5 提交表单后进入支付宝收银台。
使用手机网站支付时,外部 H5 通常不需要集成支付宝 JS-SDK。前端只需提交服务端返回的支付表单,或者跳转到支付地址。支付参数的组装与签名,以及异步通知的验签,都必须由服务端完成。
App 内嵌 WebView 对支付宝客户端唤起、URL Scheme 和支付后返回 App 的支持,与系统浏览器并不完全相同。实际项目中,优先使用系统浏览器打开手机网站支付页面,通常兼容性更好。
为什么不建议两种场景统一使用手机网站支付
手机网站支付面向浏览器和 H5 页面,支付流程通常如下:
H5 请求业务服务端
→ 服务端调用 alipay.trade.wap.pay
→ H5 提交支付表单
→ 浏览器跳转支付宝收银台
→ 用户完成支付
→ 支付宝发送异步通知
外部 App 的原生支付流程则是:
App 请求业务服务端
→ 服务端调用 alipay.trade.app.pay
→ 服务端返回 orderString
→ App 调用支付宝客户端 SDK
→ 支付宝 App 完成支付
→ SDK 返回客户端结果
→ 支付宝发送异步通知
原生 App 也可以先打开 H5 页面,再通过手机网站支付完成付款,但这种做法会带来额外的 WebView 兼容、支付宝客户端唤起、浏览器跳转和支付后返回问题。如果原生场景已经具备 App 支付能力,就没有必要再绕到 H5 支付链路。
App 内嵌 H5 的接入方式
1. H5 向业务服务端创建订单
H5 只负责传递业务订单信息。支付宝应用私钥不能保存在前端,签名也不能由前端生成。
async function createPayment() {
const response = await fetch('/api/pay/alipay/wap', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
orderId: 'ORDER_202609080001'
})
});
if (!response.ok) {
throw new Error('创建支付订单失败');
}
const html = await response.text();
// html 必须来自可信的业务服务端。
document.open();
document.write(html);
document.close();
}
服务端也可以返回一个业务支付页地址,再由前端跳转:
window.location.href = paymentUrl;
使用表单还是 URL,取决于项目所用支付宝服务端 SDK 的返回形式。不要在前端自行拼接签名参数。
2. 服务端调用手机网站支付接口
下面是 Java SDK 的结构示例。类名和请求模型可能随 SDK 版本变化,具体写法应以项目使用的官方 SDK 文档为准:
AlipayClient alipayClient = new DefaultAlipayClient(
gatewayUrl,
appId,
appPrivateKey,
"json",
"UTF-8",
alipayPublicKey,
"RSA2"
);
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setNotifyUrl("https://example.com/api/pay/alipay/notify");
request.setReturnUrl("https://example.com/pay/result");
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setOutTradeNo("ORDER_202609080001");
model.setTotalAmount("99.00");
model.setSubject("订单支付");
model.setProductCode("QUICK_WAP_WAY");
request.setBizModel(model);
AlipayTradeWapPayResponse response = alipayClient.pageExecute(request);
if (!response.isSuccess()) {
throw new IllegalStateException(
"创建支付宝支付失败:" + response.getSubMsg()
);
}
// 返回可提交的 HTML form。
String paymentForm = response.getBody();
服务端可以把 paymentForm 直接返回给 H5,也可以通过一个中间支付页面输出。由于浏览器会执行这部分内容,相关接口必须进行登录校验和订单归属校验,并防止重复下单。
3. 处理支付异步通知
支付是否成功,要以支付宝服务端发送到 notify_url 的异步通知为准。不能只看 H5 的跳转结果,也不能只依赖 App 收到的客户端回调。
处理通知时,至少要完成以下检查和操作:
- 使用支付宝公钥或支付宝公钥证书验证签名。
- 检查
app_id是否属于当前应用。 - 检查
out_trade_no是否存在,并确认它对应当前订单。 - 检查
total_amount、币种和收款方信息是否与业务订单一致。 - 判断
trade_status。 - 在幂等事务中更新订单状态。
- 处理成功后,按照接口要求返回成功响应。
伪代码如下:
Map<String, String> params = getAlipayNotifyParams(request);
boolean verified = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!verified) {
return "failure";
}
Order order = orderService.findByOrderNo(params.get("out_trade_no"));
if (order == null
|| !appId.equals(params.get("app_id"))
|| order.getAmount().compareTo(
new BigDecimal(params.get("total_amount"))
) != 0) {
return "failure";
}
String tradeStatus = params.get("trade_status");
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
orderService.markPaidIdempotently(
order.getOrderNo(),
params.get("trade_no")
);
}
return "success";
客户端在展示“支付成功”前,还可以主动查询业务服务端的订单状态,以免异步通知尚未到达时误判结果。
前端是否需要支付宝 JS-SDK
在普通外部浏览器或外部 App WebView 中使用手机网站支付,通常不需要支付宝 JS-SDK。
支付宝 JSAPI 或相关 JS-SDK 一般依赖特定的运行环境,例如支付宝客户端内的 H5 容器。在外部 App 自己的 WebView 中引入脚本,并不会自动获得支付宝客户端的原生支付能力。
这个场景可以按以下方式划分职责:
- 服务端:创建订单、调用支付接口、生成签名、验证签名并接收异步通知。
- H5 前端:请求创建订单,提交服务端返回的表单,或者执行页面跳转。
- 外部 App:处理 WebView 导航、外部协议、系统浏览器打开和支付后的页面返回。
- 支付宝客户端 SDK:仅用于原生 App 支付方案。
外部 App 原生支付示例
原生页面需要直接发起支付时,服务端应调用 alipay.trade.app.pay,再把完整的签名订单字符串返回给 App:
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
request.setNotifyUrl("https://example.com/api/pay/alipay/notify");
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setOutTradeNo("ORDER_202609080002");
model.setTotalAmount("99.00");
model.setSubject("订单支付");
model.setProductCode("QUICK_MSECURITY_PAY");
request.setBizModel(model);
AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
if (!response.isSuccess()) {
throw new IllegalStateException(
"创建支付宝 App 支付订单失败:" + response.getSubMsg()
);
}
String orderString = response.getBody();
App 应将 orderString 原样交给支付宝客户端 SDK。不要拆开参数重新排序或拼接,也不要在客户端使用应用私钥签名。
WebView 接入时的注意事项
支付页面的打开方式
部分 WebView 会拦截非 HTTP/HTTPS 协议、跨应用跳转、新窗口或重定向,导致支付宝客户端无法正常唤起。可以按以下顺序处理:
- 优先使用系统浏览器打开 H5 支付页面。
- 如果必须在 WebView 内完成流程,App 需要正确处理外部协议和页面跳转。
- 如果无法唤起支付宝客户端,应允许用户继续使用网页收银台,不能直接认定支付失败。
Android WebView 可以在导航回调中识别非 HTTP/HTTPS 地址,再交给系统处理。允许哪些地址,需要按照支付宝当前的支付链路和安全策略配置,不宜使用宽泛规则放行所有自定义协议。
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
val uri = request.url
val scheme = uri.scheme?.lowercase()
if (scheme == "http" || scheme == "https") {
return false
}
return try {
val intent = Intent(Intent.ACTION_VIEW, uri)
view.context.startActivity(intent)
true
} catch (e: ActivityNotFoundException) {
true
}
}
生产环境还要配置 Scheme 白名单、校验目标应用,并处理跳转异常。不能把任意自定义协议直接交给系统执行。
支付后的返回
return_url 只用于同步页面跳转,不能作为可信的支付结果。如果需要从 H5 返回外部 App,可以通过业务中间页配合 Universal Links、App Links 或受控的自定义 Scheme 实现。
支付结果页应先查询服务端订单状态:
async function queryOrder(orderId) {
const response = await fetch(
`/api/orders/${encodeURIComponent(orderId)}/payment-status`
);
const result = await response.json();
if (result.status === 'PAID') {
// 展示支付成功,或通过受控链接返回 App。
} else {
// 展示处理中,并允许用户重新查询。
}
}
不要直接把 return_url 中的查询参数当作支付成功凭证。
安全与兼容性
- 应用私钥只能保存在服务端。
- 金额、商品名称和订单号应由服务端根据业务订单生成。
- 同一个业务订单需要防止重复支付。
- 异步通知处理必须保证幂等。
- WebView 不能忽略 HTTPS 证书错误。
- 不能无条件放行所有 URL Scheme。
- Android 和 iOS 对外部应用跳转的限制不同,需要分别测试。
- 测试范围需要包括已安装支付宝、未安装支付宝、取消支付、支付完成后切回 App,以及异步通知延迟等情况。
- 能否直接从 WebView 唤起支付宝,还取决于系统版本、WebView 配置和支付宝当前的收银台策略,最终应以真实设备的联调结果为准。
原生页面使用 App 支付,内嵌 H5 使用手机网站支付,职责划分会更清楚。H5 前端不需要为手机网站支付额外集成支付宝 JS-SDK。
备注:内容仅供参考。
