WAP网页支付证书模式下异步回调验签失败
结论
POST 参数在验签前不需要额外加密,也不用手动排序或拼接。直接读取异步通知表单中的全部参数,按支付宝 SDK 的要求转换为参数 Map,然后调用证书模式对应的验签方法即可。
验签失败通常与参数是否经过特殊处理无关,常见原因有:
- 调用了普通公钥模式的验签方法,没有使用证书模式方法。
- 使用了应用公钥证书或商户私钥。回调验签需要使用支付宝公钥证书。
- 参数经过二次 URL 解码、重新编码或其他修改。
- 字符集、签名算法与实际配置不一致。
- 解析参数时遗漏字段,或者自行排除了
sign、sign_type。 - Web 框架提前读取或转换了请求体,改变了验签内容。
Java 验签示例
下面的示例适用于支付宝开放平台 Java SDK 的常见接口形式。SDK 版本不同,方法签名可能也会变化,请以项目当前依赖版本为准。
import com.alipay.api.internal.util.AlipaySignature;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public boolean verifyNotify(HttpServletRequest request) throws Exception {
Map<String, String> params = new HashMap<>();
Map<String, String[]> requestParams = request.getParameterMap();
for (Iterator<String> iterator = requestParams.keySet().iterator();
iterator.hasNext();) {
String name = iterator.next();
String[] values = requestParams.get(name);
StringBuilder value = new StringBuilder();
for (int i = 0; i < values.length; i++) {
if (i > 0) {
value.append(",");
}
value.append(values[i]);
}
params.put(name, value.toString());
}
String alipayPublicCertPath =
"/path/to/alipayCertPublicKey_RSA2.crt";
return AlipaySignature.rsaCertCheckV1(
params,
alipayPublicCertPath,
"UTF-8",
"RSA2"
);
}
此处必须使用支付宝公钥证书,例如:
alipayCertPublicKey_RSA2.crt
不要传入应用公钥证书,也不要使用商户私钥文件验证回调签名。
不同版本的 SDK 可能会提供名称或参数略有差异的证书验签接口。如果当前版本中没有 rsaCertCheckV1,需要确认该 SDK 是否支持证书模式,并查看对应版本的 API 定义。
正确的处理流程
1. 原样获取表单参数
支付宝异步通知通常通过 application/x-www-form-urlencoded 格式 POST 到通知地址。应通过框架提供的表单参数接口读取数据,例如:
request.getParameterMap()
不要将请求体作为 JSON 解析,也不要自行拆分请求字符串。
2. 保留完整参数
传给 SDK 的 Map 应保留回调中的全部参数,包括:
sign
sign_type
trade_no
out_trade_no
trade_status
total_amount
不要自行移除 sign 或 sign_type。SDK 会在内部提取签名值,并按照规则生成待验签字符串。
3. 使用证书模式验签接口
使用证书模式时,应调用证书验签方法,并传入支付宝公钥证书路径:
AlipaySignature.rsaCertCheckV1(
params,
alipayPublicCertPath,
charset,
signType
);
普通公钥模式常用的 rsaCheckV1 接收支付宝公钥字符串,不能用应用公钥证书内容直接代替。
4. 保证字符集和算法一致
常见配置是:
charset = UTF-8
signType = RSA2
这两个值必须与支付请求及开放平台中的配置一致,不能只凭经验写死。如果实际请求使用了其他值,应按真实配置传入。
5. 验签成功后再校验业务字段
签名验证成功,只能说明通知内容通过了身份和完整性验证。之后还需要检查业务字段:
String appId = params.get("app_id");
String outTradeNo = params.get("out_trade_no");
String totalAmount = params.get("total_amount");
String sellerId = params.get("seller_id");
String tradeStatus = params.get("trade_status");
至少需要确认:
app_id是当前应用的 AppID。out_trade_no在本地存在。total_amount与本地订单金额一致。- 收款方信息符合预期。
- 订单没有被重复处理。
trade_status属于业务允许处理的状态。
常见错误
对参数进行二次解码
下面的处理可能会破坏包含 +、%、空格或中文的字段:
URLDecoder.decode(value, "UTF-8");
如果 Servlet 容器或 Web 框架已经完成表单解码,就不要再次调用 URLDecoder.decode()。
自己排序和拼接参数
不要自行编写类似下面的处理:
params.entrySet()
.stream()
.sorted(...)
.map(...)
.collect(...);
如果没有完全按照支付平台的签名规范处理空值、编码和特殊字符,生成的待验签字符串很容易与 SDK 不一致。使用官方 SDK 时,直接传入参数 Map 即可。
使用错误的证书
证书模式会涉及多份文件,每份文件的用途不同:
- 应用私钥:商户发起请求时用于签名。
- 应用公钥证书:平台用于识别应用公钥。
- 支付宝公钥证书:商户用于验证支付宝返回数据和异步通知的签名。
- 支付宝根证书:用于证书链、序列号等相关处理,不能直接替代支付宝公钥证书验证回调签名。
异步通知验签需要使用支付宝公钥证书。
验签前修改参数
以下操作都可能导致验签失败:
- 对字段调用
trim()。 - 将空字符串转换为
null。 - 修改金额格式,例如将
0.01改成0.010。 - 修改换行符或中文字符。
- 把参数转换成 JSON 后再验签。
- 根据业务对象重新生成参数 Map。
- 使用日志中经过转义或截断的数据验签。
验签时应使用从当前 HTTP 请求中直接解析得到的原始表单参数 Map。
回调响应
业务处理完成后,需要按照接口约定返回成功响应。支付宝异步通知通常要求返回以下纯文本:
success
示例:
if (!verifyNotify(request)) {
response.getWriter().write("failure");
return;
}
try {
// 校验订单并执行幂等的业务处理
response.getWriter().write("success");
} catch (Exception e) {
response.getWriter().write("failure");
}
不要在 success 前后添加 HTML、JSON、空格或其他说明文字。只有验签成功并完成业务处理后才能返回成功,否则平台可能停止重试,导致订单状态没有正确更新。
备注:内容仅供参考。