PAYATHON 2026

iOS App“手机网站支付”返回的 URL 无法在 Safari 打开

支付小周

结论

不要把示例中的支付宝网关地址当作普通 URL 拼接后,直接交给 Safari 打开。

使用“手机网站支付”时,服务端应调用 alipay.trade.wap.pay,通过支付宝服务端 SDK 生成完整且已签名的支付表单或支付 URL,再由 iOS 加载。如果是纯原生 iOS App 支付场景,通常更适合使用“App 支付”:服务端生成 orderString,客户端再通过支付宝 SDK 发起支付。

原因分析

下面的地址只是支付宝沙箱环境的网关入口:

https://openapi.alipaydev.com/gateway.do

一个可用的手机网站支付请求还需要完整的公共参数、业务参数和签名,例如:

app_id
method
format
charset
sign_type
timestamp
version
biz_content
sign

如果缺少参数、签名有误、URL 被截断或参数编码不正确,即使 Safari 能访问网关,也无法正常打开支付页面。

还要逐项检查这些问题:

  • openapi.alipaydev.com 是沙箱网关,生产环境不能使用这个地址。
  • 如果 URL 中实际出现了 alipay-sdk-java-dynamicVersionNo,并非日志或示例里的占位内容,可能是 SDK 构建、依赖或资源替换出了问题,不应继续使用这个请求。
  • 手机网站支付 SDK 的 pageExecute() 通常返回 HTML 表单,未必是可以直接传给 Safari 的纯 URL。
  • URL 中混入空格、引号或换行,或者 biz_content、sign 被重复编码,都会导致请求失效。
  • return_url 是支付完成后返回商户页面的地址,不是支付宝网关地址。
  • 沙箱应用的 app_id、应用私钥和支付宝公钥必须来自同一套沙箱配置,不能和生产配置混用。

推荐解决方案

方案一:继续使用手机网站支付

支付请求必须由服务端生成,私钥不能放在 iOS 客户端。

以 Java SDK 为例:

AlipayClient alipayClient = new DefaultAlipayClient(
    "https://openapi.alipaydev.com/gateway.do",
    APP_ID,
    APP_PRIVATE_KEY,
    "json",
    "UTF-8",
    ALIPAY_PUBLIC_KEY,
    "RSA2"
);

AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();

request.setReturnUrl("https://merchant.example.com/pay/return");
request.setNotifyUrl("https://merchant.example.com/pay/notify");

AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setOutTradeNo("ORDER_202609090001");
model.setTotalAmount("0.01");
model.setSubject("测试订单");
model.setProductCode("QUICK_WAP_WAY");
model.setQuitUrl("https://merchant.example.com/pay/cancel");

request.setBizModel(model);

String paymentForm = alipayClient.pageExecute(request).getBody();

服务端把 paymentForm 作为 HTML 返回给 iOS,响应头可以使用:

Content-Type: text/html; charset=UTF-8

iOS 端通过 WKWebView 加载商户服务端的支付入口:

import WebKit

let request = URLRequest(
    url: URL(string: "https://merchant.example.com/pay/start?orderId=ORDER_202609090001")!
)

webView.load(request)

服务端也可以提供一个支付页面地址,再由系统浏览器打开:

if let url = URL(string: paymentPageURL) {
    UIApplication.shared.open(url, options: [:], completionHandler: nil)
}

这里的 paymentPageURL 应指向商户服务端的支付入口,不能由客户端自行拼接支付宝网关地址。

如果服务端 SDK 使用 URL 模式,需要确认调用的是该 SDK 对应的 URL 生成接口,并检查返回值是否包含完整的签名参数。不要从 HTML 表单中截取 action 属性作为最终支付 URL,因为业务参数可能放在隐藏的表单字段里。

方案二:原生 App 改用 App 支付

如果整个业务入口都在 iOS App 内,可以使用支付宝“App 支付”产品。

服务端调用 alipay.trade.app.pay,生成签名后的 orderString:

AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();

AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setOutTradeNo("ORDER_202609090001");
model.setTotalAmount("0.01");
model.setSubject("测试订单");
model.setProductCode("QUICK_MSECURITY_PAY");

request.setBizModel(model);
request.setNotifyUrl("https://merchant.example.com/pay/notify");

AlipayTradeAppPayResponse response =
    alipayClient.sdkExecute(request);

String orderString = response.getBody();

iOS 客户端拿到 orderString 后,将它交给支付宝客户端 SDK。不同版本的支付宝 iOS SDK 对 Swift 和 Objective-C 暴露的接口可能不同,具体调用方式应以项目当前接入的 SDK 为准。

环境配置检查

沙箱环境通常使用:

https://openapi.alipaydev.com/gateway.do

生产环境通常使用:

https://openapi.alipay.com/gateway.do

切换环境时,以下配置需要成套更换:

  • gatewayUrl
  • app_id
  • 应用私钥
  • 支付宝公钥或证书配置
  • 对应环境的应用及产品权限

不能只替换网关域名。

排查现有 URL

可以按下面的顺序排查:

  1. 确认日志没有截断 URL。完整地址中应包含 method=alipay.trade.wap.pay 和 sign。
  2. 检查 URL 的开头和结尾是否混入引号、空格或换行。
  3. 检查是否对整个 URL 再次进行了百分号编码。
  4. 确认 sign 中的 +、/、= 等字符已经正确进行 URL 编码。
  5. 确认请求参数和生成签名时使用的参数完全一致。
  6. 确认沙箱与生产环境的密钥、app_id 没有混用。
  7. 如果 alipay_sdk 的值确实包含 dynamicVersionNo,应重新检查支付宝 Java SDK 的依赖来源和打包结果,并优先使用官方发布的 SDK 构件。
  8. 查看支付宝网关返回的错误码和错误信息,不能只根据 Safari 显示的“无法打开页面”判断原因。

注意事项

支付结果必须以服务端异步通知为准。验签通过后,还要核对 app_id、商户订单号、订单金额和交易状态。不能因为 Safari 跳转到了 return_url,或者客户端显示支付成功,就直接把订单更新为已支付。

应用私钥不能保存在客户端,支付宝签名也不能由客户端自行生成。客户端只负责加载服务端生成的支付页面,或者把服务端生成的 orderString 交给支付宝 App 支付 SDK。

备注:内容仅供参考。