iOS 调用 AlipaySDK 报 isv.invalid-signature
结论
这不是 iOS 端缺少支付参数或 URL Scheme 配置造成的。isv.invalid-signature 表示支付宝收到的订单参数,与服务端生成签名时使用的参数不一致。
从支付宝返回的验签字符串来看,iOS 在调用 payOrder:fromScheme:callback: 前,很可能对整个 orderString 做了 URL 解码,或者网络层、模型转换层自动执行了解码。这样一来,notify_url 中经过编码的 %26 会被还原为 &,其中的 duration、payment_id、persona_id 等查询参数也会被支付宝当作订单的一级参数,最终导致验签失败。
服务端返回支付宝订单字符串后,iOS 应将其原样传给 AlipaySDK,不要对整个字符串进行 URL 编码或解码。
为什么可以判断订单字符串被解码了
服务端返回的 notify_url 是:
notify_url=https%3A%2F%2Ftest.xiaohongmei.net%2Fapi%2Fpay%2Fcallback%3Fuser_id%3D...%26plan_template_id%3D...%26persona_template_id%3D...%26payment_id%3D...%26persona_id%3D...%26duration%3D2592000
其中:
%3F表示?%3D表示=%26表示&
这些编码不能省略,因为 notify_url 本身带有查询参数,需要作为一个完整的支付宝请求参数传递。
支付宝返回的验签字符串中却出现了:
notify_url=https://test.xiaohongmei.net/api/pay/callback?user_id=...
&payment_id=...
&persona_id=...
&persona_template_id=...
&plan_template_id=...
&duration=2592000
原本属于 notify_url 的参数已经被拆成支付宝订单的一级参数。支付宝实际参与验签的内容因此发生了变化,无法通过服务端生成的 RSA2 签名。
签名字段本身也可能被同时破坏。例如:
%2F
%2B
%3D
提前解码后会变成:
/
+
=
尤其是 +,在后续表单解析过程中还可能被当作空格,进一步改变签名内容。
正确的调用方式
服务端应完成以下工作:
- 组装业务参数。
- 使用应用私钥生成 RSA2 签名。
- 对各参数值进行必要的 URL 编码。
- 返回可以直接提交给 AlipaySDK 的完整订单字符串。
iOS 收到后直接调用:
NSString *appScheme = @"shilaixiaohongmei";
// orderString 必须是服务端返回的原始字符串
[[AlipaySDK defaultService] payOrder:orderString
fromScheme:appScheme
callback:^(NSDictionary *resultDic) {
NSLog(@"result = %@", resultDic);
}];
不要执行下面这类处理:
// 错误:会破坏 notify_url 和 sign 等参数
orderString = [orderString stringByRemovingPercentEncoding];
也不要再次编码完整的订单字符串:
// 错误:可能造成二次编码
orderString = [orderString stringByAddingPercentEncodingWithAllowedCharacters:
[NSCharacterSet URLQueryAllowedCharacterSet]];
如果接口返回 JSON,只需用 JSON 解析器解析 JSON,不要再对其中的订单字符串做 URL 解码:
NSDictionary *responseObject = ...;
NSString *orderString = responseObject[@"data"][@"order_string"];
if (![orderString isKindOfClass:[NSString class]] || orderString.length == 0) {
NSLog(@"Invalid Alipay order string");
return;
}
[[AlipaySDK defaultService] payOrder:orderString
fromScheme:@"shilaixiaohongmei"
callback:^(NSDictionary *resultDic) {
NSLog(@"result = %@", resultDic);
}];
建议的排查方法
服务端生成订单字符串后,先计算一次 SHA-256。iOS 调用 SDK 前,再对收到的字符串计算一次 SHA-256。两端摘要必须完全相同。
还要检查以下内容:
- 字符串长度是否一致。
%26是否仍然存在。%2B、%2F、%3D是否被提前还原。+是否被转换成空格。- 是否调用过
stringByRemovingPercentEncoding。 - 网络封装层是否自动执行了 URL decode。
- 是否先将订单字符串拆成字典,再重新拼接。
- 是否使用
NSURLComponents、queryItems等 API 重新处理过订单字符串。
调试时,可以临时输出调用 SDK 前的内容,并与服务端的原始响应逐字符比较:
NSLog(@"order length = %lu", (unsigned long)orderString.length);
NSLog(@"order string = %@", orderString);
生产环境中不要长期记录完整的订单字符串,其中包含签名和交易信息。
URL Scheme 是否有问题
当前 CFBundleURLSchemes 与调用时使用的 appScheme 一致:
<string>shilaixiaohongmei</string>
URL Scheme 主要用于支付宝完成支付后唤起应用,不参与订单的 RSA2 验签,因此不会引发 isv.invalid-signature。
配置中的空节点没有作用,可以删除:
<dict/>
整理后的配置可以写成:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>alipay</string>
<key>CFBundleURLSchemes</key>
<array>
<string>shilaixiaohongmei</string>
</array>
</dict>
</array>
删除空节点只是清理配置,不能解决当前的验签错误。
还需要注意什么
Android 可以正常支付,说明服务端生成的原始订单字符串大概率没有问题,也进一步将排查范围缩小到了 iOS 收到订单字符串后的处理链路。
服务端签名时还要遵循两个原则:
- 签名必须使用与
app_id对应的应用私钥。 - 最终传给支付宝的参数值,必须与签名时使用的参数值完全一致。
不要在客户端解析、修改或重新拼接已经签名的订单字符串。即使只改动 URL、空格、中文编码或 JSON 格式,也可能造成验签失败。
当前案例应优先检查 iOS 端是否对整个 orderString 调用了 URL 解码。支付宝返回的验签字符串已经表明 notify_url 被拆分,这是最明确的异常。将处理方式改为”服务端返回什么,iOS 就原样传什么”后,通常不需要再增加其他 iOS 支付参数。
备注:内容仅供参考。
