支付宝沙箱支付链接报错 invalid-signature 如何排查?
结论
invalid-signature 通常不是密钥生成有误,而是参与签名的内容与支付宝实际收到的内容不一致。排查时重点看以下几项:
- 使用的
app_id和应用私钥是否属于当前沙箱应用 - 是否误用支付宝公钥生成请求签名
- 是否使用沙箱网关
sign_type是否与密钥和 SDK 配置一致- 请求参数是否在签名后被编码、修改或重新序列化
- 是否混用了普通公钥模式和证书模式
biz_content、timestamp、charset、接口名称等参数是否正确- SDK 生成的是支付表单还是 URL,实际调用方式是否匹配
请求签名必须使用应用私钥。支付宝公钥用于验证支付宝返回数据或异步通知的签名,不能生成支付请求签名。
首先核对环境配置
沙箱环境的应用信息必须配套使用,不能与正式环境或其他应用的配置混在一起:
沙箱 app_id
沙箱应用对应的应用私钥
该沙箱应用页面展示的支付宝公钥
沙箱网关
沙箱网关通常配置为:
https://openapi-sandbox.dl.alipaydev.com/gateway.do
如果 app_id 来自沙箱,而网关、支付宝公钥或应用私钥来自正式环境或其他应用,即使程序能生成签名,支付宝也无法通过验证。
可以按下面的顺序重新核对:
- 登录支付宝开放平台沙箱环境。
- 找到当前使用的沙箱应用。
- 核对代码中的
app_id。 - 确认上传到该应用的应用公钥与当前应用私钥属于同一密钥对。
- 重新复制该应用页面展示的支付宝公钥。
- 检查程序是否仍从环境变量、配置中心或旧配置文件中读取另一套密钥。
除了检查代码中写入的配置,还要确认程序运行时实际加载的值。
明确两个公钥的用途
普通公钥模式下,各密钥的用途如下:
| 密钥 | 用途 |
|---|---|
| 应用私钥 | 商户系统对请求参数签名 |
| 应用公钥 | 上传至支付宝开放平台,供支付宝验证商户请求 |
| 支付宝公钥 | 商户系统验证支付宝响应或通知的签名 |
常见错误是把支付宝公钥传给“应用私钥”参数,或者把应用公钥当作支付宝公钥使用。
以 Java SDK 为例,初始化顺序通常如下:
AlipayClient alipayClient = new DefaultAlipayClient(
"https://openapi-sandbox.dl.alipaydev.com/gateway.do",
APP_ID,
APP_PRIVATE_KEY,
"json",
"UTF-8",
ALIPAY_PUBLIC_KEY,
"RSA2"
);
第三个参数必须是应用私钥,第六个参数才是支付宝公钥。
检查密钥格式
从密钥工具复制的内容可能包含 PEM 头尾:
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
不同 SDK 对密钥格式的要求并不完全相同。有些 SDK 接受完整 PEM,有些只接受 Base64 正文,应以当前 SDK 的初始化参数说明为准。
还需要检查:
- 密钥中是否有多余空格
- 换行是否被配置中心转换成字面量
\n - 是否遗漏了某一行
- 是否误把公钥内容当作私钥
- 私钥是否为 PKCS#8 格式
- 复制密钥时是否混入全角字符或不可见字符
如果私钥保存在环境变量中,可以在加载后恢复换行,但不要把完整私钥输出到日志:
String privateKey = System.getenv("ALIPAY_APP_PRIVATE_KEY");
if (privateKey != null) {
privateKey = privateKey.replace("\\n", "\n").trim();
}
生产日志最多记录密钥指纹、长度或首尾类型标记,不能记录私钥正文。
确认 RSA2 配置一致
目前常见的签名类型是:
sign_type=RSA2
RSA2 对应 SHA256WithRSA。如果代码使用 RSA2 生成签名,请求参数也必须声明 RSA2,不能出现这种情况:
代码按 RSA2 签名
请求参数却传 sign_type=RSA
反过来也不行。使用官方 SDK 时,最好由 SDK 完成参数组装和签名,不要自行覆盖它生成的 sign 或 sign_type。
检查普通公钥模式和证书模式是否混用
支付宝 SDK 通常支持普通公钥模式和证书模式,两种模式的初始化方式不同。
普通公钥模式使用:
应用私钥
支付宝公钥
证书模式通常使用:
应用私钥
应用公钥证书
支付宝公钥证书
支付宝根证书
如果沙箱应用配置的是普通应用公钥,就应使用普通公钥模式初始化客户端。不要把证书文件路径传给普通公钥参数,也不要在使用证书模式初始化后继续传入普通支付宝公钥。
SDK 是否支持某种证书模式,以及具体该如何初始化,取决于所用版本,应查看对应语言 SDK 的接口说明。
不要在签名后修改参数
支付宝验签时,会根据收到的参数重新构造待验签字符串,再使用应用公钥验证 sign。签名完成后,只要有参数发生变化,就可能出现 invalid-signature。
常见情况包括:
- 签名后又修改
biz_content - 先对参数进行 URL 编码,再让编码后的内容参与签名
- 对生成的完整 URL 再次执行
URLEncoder.encode - Web 框架自动把
+转成空格 - 手工拼接 URL 时遗漏
&或= - JSON 序列化前后的字段、转义字符或数值格式发生变化
- 代理、模板引擎或前端代码对参数进行二次编码
- 将
%2B、%2F、%3D解码后重新拼接
正确流程通常是:
- 准备业务参数。
- 构造公共请求参数。
- 按规则生成待签名字符串。
- 使用应用私钥签名。
- 把签名结果作为
sign参数加入请求。 - 最后对各个 URL 参数值进行一次编码。
不要再次整体编码 SDK 已经生成的完整请求字符串。例如:
// 错误示例:可能破坏 SDK 已经完成的参数编码
String encodedUrl = URLEncoder.encode(generatedUrl, StandardCharsets.UTF_8);
使用官方 SDK 时,通常可以直接输出 SDK 返回的表单或请求内容。
使用 SDK 生成电脑网站支付请求
电脑网站支付接口通常为:
alipay.trade.page.pay
业务参数通常包括:
out_trade_no
total_amount
subject
product_code
示例:
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
public class AlipaySandboxDemo {
private static final String GATEWAY =
"https://openapi-sandbox.dl.alipaydev.com/gateway.do";
private static final String APP_ID = "你的沙箱APP_ID";
private static final String APP_PRIVATE_KEY = "你的应用私钥";
private static final String ALIPAY_PUBLIC_KEY = "沙箱应用页面中的支付宝公钥";
public static String createPayForm() throws Exception {
AlipayClient client = new DefaultAlipayClient(
GATEWAY,
APP_ID,
APP_PRIVATE_KEY,
"json",
"UTF-8",
ALIPAY_PUBLIC_KEY,
"RSA2"
);
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl("https://example.com/alipay/return");
request.setNotifyUrl("https://example.com/alipay/notify");
request.setBizContent("""
{
"out_trade_no": "TEST202609090001",
"total_amount": "0.01",
"subject": "沙箱支付测试",
"product_code": "FAST_INSTANT_TRADE_PAY"
}
""");
return client.pageExecute(request).getBody();
}
}
不少 Java SDK 版本的 pageExecute() 返回一段自动提交的 HTML 表单,而不是可以随意拼接的纯 URL。服务端应将这段 HTML 内容直接返回给浏览器:
@GetMapping(value = "/pay", produces = "text/html;charset=UTF-8")
@ResponseBody
public String pay() throws Exception {
return AlipaySandboxDemo.createPayForm();
}
如果把 HTML 当成 URL 传给前端跳转,页面将无法正常打开。因此,需要先确认 SDK 返回的是哪一种内容:
- 完整支付 URL
- HTML
<form>表单 - API JSON 响应
这三种内容的使用方式不同。
手工签名时检查待签名字符串
只有在无法使用 SDK 时,才建议手工签名。待签名参数一般需要满足以下条件:
- 排除
sign - 排除值为空的参数
- 按参数名排序
- 使用原始参数值拼接
- 签名完成后,再对请求参数值进行 URL 编码
示意代码如下:
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public static String buildContentToSign(Map<String, String> parameters) {
return new TreeMap<>(parameters)
.entrySet()
.stream()
.filter(entry -> !"sign".equals(entry.getKey()))
.filter(entry -> entry.getValue() != null && !entry.getValue().isEmpty())
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&"));
}
public static String rsa2Sign(
String content,
PrivateKey privateKey
) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signature.sign());
}
这段代码只用于说明签名过程。私钥解析、参数过滤和编码细节仍应与支付宝当前接口规范及所用 SDK 保持一致。
还要特别注意,参与签名的 biz_content 字符串必须与最终发送的字符串完全一致。下面两段 JSON 的业务含义相同,但原始字符串并不相同:
{"total_amount":"0.01","subject":"测试"}
{
"subject": "测试",
"total_amount": "0.01"
}
如果第一段参与签名,最终发送的却是重新序列化后的第二段,验签可能失败。
核对公共参数
电脑网站支付请求至少需要重点检查这些公共参数:
app_id
method
format
charset
sign_type
timestamp
version
biz_content
sign
典型值如下:
method=alipay.trade.page.pay
format=json
charset=UTF-8
sign_type=RSA2
version=1.0
timestamp 通常使用以下格式:
yyyy-MM-dd HH:mm:ss
不要用 Unix 时间戳代替,也不要在签名完成后修改时区格式或字符串内容。
区分签名失败与跳转失败
签名失败和跳转失败可能有关,也可能是两个独立问题。
如果支付宝返回:
invalid-signature
应优先检查密钥对应关系、签名算法、待签名字符串和编码过程。
如果没有签名错误,但浏览器仍无法跳转,再检查:
- SDK 返回的是表单还是 URL
- 后端是否把 HTML 表单当作 JSON 返回
- 前端是否转义了返回内容
- 是否对整个支付地址进行了二次编码
- 页面是否受 CSP、iframe 限制或浏览器拦截
return_url是否为合法的绝对地址- 沙箱网关域名是否被代理或网关规则改写
notify_url 无法从公网访问,通常会影响异步通知,但一般不会直接引起支付请求的 invalid-signature。本地调试时可以暂时不设置通知地址,以缩小排查范围。是否允许省略,应以当前接口要求为准。
推荐的排查顺序
- 使用沙箱网关,确认请求没有发往正式网关。
- 打印脱敏后的运行时
app_id、网关、sign_type和charset。 - 确认第三方 SDK 初始化参数中传入的是应用私钥和支付宝公钥。
- 确认应用私钥与沙箱后台上传的应用公钥属于同一密钥对。
- 确认没有混用证书模式和普通公钥模式。
- 删除手工签名、手工排序和二次 URL 编码逻辑,先完全使用官方 SDK。
- 检查
pageExecute()的返回值,并按 HTML 表单方式直接响应给浏览器。 - 如果仍然失败,记录实际发送的公共参数和待签名字符串,但必须隐藏
sign、私钥及敏感业务数据。 - 使用同一份参数在本地重新验签,对比签名前后的字符串是否发生变化。
- 最后检查代理层、前端框架或跳转代码是否改写了请求内容。
这类问题多数出在三个地方:密钥与 app_id 不配套,签名后的参数被再次编码,或者 SDK 返回的支付表单被当成普通链接使用。
备注:内容仅供参考。
