H5 deeplink 唤起支付宝免密签约报 ILLEGAL_EXTERFACE
结论
问题大概率不在签约接口本身,而在这个 APP 的 WebView 对 alipays:// 链接的处理。URI 可能被截断、解码、重新编码或拆分参数,导致支付宝最终收到的接口参数不完整。
这条链接包含两层查询参数:
- 外层:
alipays://platformapi/startapp - 内层:
https://mapi.alipay.com/gateway.do?...
内层 URL 必须作为一个整体进行百分号编码。否则,其中的 &、=、|、JSON 以及 Base64 签名字符可能会被当作外层参数解析。支付宝无法获得正确的 service、sign 等字段时,就可能返回 ILLEGAL_EXTERFACE。
同一服务端生成的链接在其他 APP 中能够使用,只能说明业务参数和商户配置基本有效,不能证明故障 APP 交给支付宝的 URI 与原链接完全一致。
首先检查最终唤起的 URI
不要只比较服务端的返回值。需要分别记录:
- 服务端返回的原始 deeplink;
- H5 页面实际使用的字符串;
- WebView 拦截回调收到的 URI;
- 调用系统跳转 API 前的 URI;
- 支付宝客户端最终收到的参数,如能通过日志或抓包确认。
检查时重点留意:
url=后的内层 URL 是否完整编码;- URI 是否在第一个内层
&处被拆开; - 是否执行了两次
decodeURIComponent; %26是否提前还原成&;+是否变成了空格;- Base64 签名中的
+、/、=是否被破坏; - JSON 中的引号、空格和花括号是否发生变化;
- WebView 是否把 URI 交给了 HTTP 加载流程,而不是系统 Scheme;
- 字符串中是否确实存在示例里的空格,例如
appId =、url =。如果空格只是排版产生的,可以忽略;如果原始 URI 中也有这些空格,则必须去除或正确编码。
正确构造两层 URL
先生成完整的支付宝网关 URL,再将整个网关 URL 编码为外层 url 参数的值。
const gatewayParams = new URLSearchParams({
_input_charset: "utf-8",
access_info: JSON.stringify({ channel: "ALIPAYAPP" }),
external_sign_no: externalSignNo,
notify_url: notifyUrl,
partner: partner,
product_code: "GENERAL_WITHHOLDING_P",
return_url: returnUrl,
scene: "INDUSTRY|B2B_TICKET_AGENT",
service: "alipay.dut.customer.agreement.page.sign",
sign: sign,
sign_type: "RSA"
});
const gatewayUrl =
`https://mapi.alipay.com/gateway.do?${gatewayParams.toString()}`;
const deepLink =
`alipays://platformapi/startapp?appId=20000067&url=${encodeURIComponent(gatewayUrl)}`;
生成结果应采用类似下面的结构:
alipays://platformapi/startapp?appId=20000067&url=https%3A%2F%2Fmapi.alipay.com%2Fgateway.do%3F_input_charset%3Dutf-8%26...
不要直接拼成这种形式:
alipays://platformapi/startapp?appId=20000067&url=https://mapi.alipay.com/gateway.do?_input_charset=utf-8&service=...
从 &_input_charset 或后续的 &service 开始,解析器可能会将这些内容识别为 startapp 的外层参数。
签名顺序也需要注意。通常应先按照接口规则对业务参数签名,再对传输时使用的参数值进行 URL 编码。不要对编码后的整段查询字符串重新签名,也不要在不同端重复编码或解码。
Android WebView 的处理方式
Android 端应在 WebView 导航回调中识别 alipays Scheme,并将其交给系统 Intent。不要再调用 WebView.loadUrl()。
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
val uri = request?.url ?: return false
if (uri.scheme.equals("alipays", ignoreCase = true)) {
return try {
val intent = Intent(Intent.ACTION_VIEW, uri)
view?.context?.startActivity(intent)
true
} catch (e: ActivityNotFoundException) {
// 支付宝未安装,可在这里展示提示或进入受信任的备用页面
true
}
}
return false
}
旧版回调也应采用相同的处理原则:
override fun shouldOverrideUrlLoading(
view: WebView?,
url: String?
): Boolean {
if (url.isNullOrBlank()) return false
val uri = Uri.parse(url)
if (uri.scheme.equals("alipays", ignoreCase = true)) {
return try {
view?.context?.startActivity(Intent(Intent.ACTION_VIEW, uri))
true
} catch (e: ActivityNotFoundException) {
true
}
}
return false
}
拦截后不要先对整个 URI 解码:
// 不建议:可能把内层编码恢复得过早,破坏两层参数边界
val decoded = URLDecoder.decode(url, "UTF-8")
还要检查宿主 APP 中是否有统一路由、安全 SDK、URL 白名单或重定向模块。这些组件可能会重新创建 URI,进而改变参数内容。
iOS WKWebView 的处理方式
iOS 应在导航代理中拦截非 HTTP Scheme,并通过系统 API 打开原始 URL。
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
guard let url = navigationAction.request.url else {
decisionHandler(.cancel)
return
}
if url.scheme?.lowercased() == "alipays" {
UIApplication.shared.open(url, options: [:]) { success in
if !success {
// 支付宝未安装或系统拒绝打开
}
}
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}
不要先调用 removingPercentEncoding 再打开:
// 不建议:可能提前解码内层 URL
let decoded = url.absoluteString.removingPercentEncoding
如果 APP 配置了 Scheme 白名单、通用路由器或 URL 安全校验,还要确认 alipays 没有被拒绝或重写。LSApplicationQueriesSchemes 主要影响 canOpenURL 查询。是否需要配置,要看代码有没有调用该方法,不能将它简单归结为所有唤起失败的原因。
H5 侧的建议
页面只需使用已经构造好的 deeplink,不要反复编码整条链接:
function openAlipay(deepLink) {
window.location.href = deepLink;
}
如果服务端返回的 deeplink 已经完成编码,前端不要再次执行:
// 不建议:可能造成整条 URI 双重编码
window.location.href = encodeURIComponent(deepLink);
如果字符串通过 JS Bridge 传给原生端,还要检查 Bridge 的序列化过程是否改写了 +、%、& 或引号。
仍需排查的接口侧问题
如果已经确认故障 APP 和正常 APP 传给支付宝的 URI 逐字符一致,再检查:
- 商户是否开通了对应的免密签约产品;
service是否与当前签约产品匹配;partner、签约场景和产品码是否属于同一商户配置;external_sign_no是否重复或状态异常;- 签名算法、字符集和待签名字符串是否一致;
notify_url、return_url是否经过了非预期编码;- 接口是否仍允许通过当前入口调用。
alipay.dut.customer.agreement.page.sign 是否适用于当前商户和产品,应以该商户接入时获得的支付宝接口文档及开放平台配置为准,不能只根据错误字符串判断。
最有效的定位方式,是分别取出正常 APP 和故障 APP 在调用系统唤起 API 前的完整 URI,进行逐字符或字节级比较。如果两者不同,问题就在 H5、JS Bridge、WebView 拦截器或 APP 路由层;如果完全相同,再排查商户权限、支付宝客户端环境以及接口参数状态。
备注:内容仅供参考。
