支付回调结果验签失败并抛出 rsa VerificationError
结论
rsa.pkcs1.VerificationError: Verification failed 表示:程序使用当前支付宝公钥验证签名时,得到的摘要与根据 result 计算出的摘要不一致。
问题通常不在 Sanic 或 rsa 库本身,而在以下几个方面:
- 使用了错误的支付宝公钥,例如应用公钥、私钥、证书公钥或其他环境的公钥。
- 参与验签的
result与支付宝实际签名的原始内容不同。 signature在 URL 解码、Base64 解码或参数传递过程中被修改。- 回调参数拼接有误,例如没有排除
sign、sign_type,或出现重复编码、排序错误。 - 支付宝的签名模式与当前应用配置不一致。
- 字符串转为字节时使用了错误的字符集。
排查时先确认公钥是否正确,再检查待验签字符串是否保持原样。这两类问题最常见。
1. 确认使用的是支付宝公钥
验签必须使用与当前应用和运行环境对应的支付宝公钥,不能使用:
- 应用公钥
- 应用私钥
- 商户私钥
- 其他应用的支付宝公钥
- 生产回调对应的沙箱公钥
- 与当前普通公钥模式或证书模式不匹配的公钥
配置通常如下:
ALIPAY_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
支付宝公钥正文
-----END PUBLIC KEY-----"""
还要检查密钥正文是否被破坏。例如,下面的字符串包含字面量 \n,不是真正的换行:
# 错误示例:字符串中包含字面量 \n,而不是真正的换行
ALIPAY_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\\nMIIB...\\n-----END PUBLIC KEY-----"
如果密钥保存在环境变量中,可以根据实际存储方式在加载时恢复换行:
public_key = os.environ["ALIPAY_PUBLIC_KEY"].replace("\\n", "\n")
不要把完整密钥写入日志。排查多套环境时,可以记录公钥摘要:
import hashlib
fingerprint = hashlib.sha256(public_key.encode("utf-8")).hexdigest()
print("alipay public key fingerprint:", fingerprint)
对比摘要即可判断不同服务器是否加载了同一把公钥,也不会泄露密钥内容。
2. 检查待验签内容是否正确
待验签内容必须与支付宝签名时使用的内容逐字节一致。以下变化都会导致验签失败:
- 增加或删除空格
- 重新序列化 JSON
- 改变参数顺序
- 改变中文转义形式
- 将
/转义为\/ - 将小数格式从
1.00改成1.0 - 重新转换布尔值或
null - 改变换行符
- 使用不同的字符编码
如果 result 是支付宝返回的原始 JSON 字符串,不要先调用 json.loads(),再对结果执行 json.dumps() 后验签:
# 不推荐:重新序列化后的内容不保证与原文完全一致
data = json.loads(result)
message = json.dumps(data)
Alipay.verify_with_rsa(message, signature)
应保留并使用接口规定的原始待验签字符串:
message = result # 保留收到时的原始文本
verified = Alipay.verify_with_rsa(message, signature)
同时要查清 verify_with_rsa() 的 message 参数究竟需要原始响应字段,还是按规则拼接后的参数字符串。不同 SDK 和封装方法的输入约定可能不同,不能只根据方法名判断。
3. 异步通知需要按规则构造待验签字符串
处理支付宝异步通知时,通常不能直接验证整个请求体或任意 JSON 对象。应从表单参数中删除 sign 和 sign_type,然后按参数名排序并拼接。
示例:
from urllib.parse import parse_qs
raw_body = request.body.decode("utf-8")
parsed = parse_qs(raw_body, keep_blank_values=True)
params = {
key: values[0]
for key, values in parsed.items()
}
signature = params.pop("sign", None)
sign_type = params.pop("sign_type", None)
message = "&".join(
f"{key}={params[key]}"
for key in sorted(params)
)
verified = Alipay.verify_with_rsa(message, signature)
这段代码只展示基本思路。实际项目应优先使用当前支付宝 SDK 提供的通知验签方法,因为 SDK 可能已经处理了参数过滤、排序、编码和签名模式等细节。
如果框架已将表单解析为字典,也可以直接用表单参数构造待验签字符串:
params = dict(request.form)
signature = params.pop("sign", None)
params.pop("sign_type", None)
message = "&".join(
f"{key}={params[key]}"
for key in sorted(params)
)
使用前要确认 dict(request.form) 中的值是否为字符串。有些框架会将表单值保存为列表或特殊对象,需要按照实际数据结构处理。
4. 检查签名是否在传递过程中被修改
支付宝签名通常是 Base64 字符串,可能包含以下字符:
+ / =
常见错误是重复执行 URL 解码,导致 + 被转换为空格:
# 可能造成签名损坏
signature = unquote_plus(unquote_plus(signature))
可以检查签名的基本特征,同时避免将完整签名写入日志:
print("signature length:", len(signature))
print("contains space:", " " in signature)
print("ends with padding:", signature.endswith("="))
如果签名中出现空格,应检查:
- 请求是否按
application/x-www-form-urlencoded正确解析。 - 代码是否手动调用了
unquote_plus()。 - 网关、代理或业务代码是否进行了第二次 URL 解码。
- 前端或中间服务是否把
+当成普通空格传递。
不建议将下面的处理作为长期修复:
signature = signature.replace(" ", "+")
它只能帮助确认问题是否由错误解码引起。正确的处理方式是找到签名被修改的环节,避免重复解码。
还可以先验证签名是否仍是合法的 Base64:
import base64
import binascii
try:
decoded_signature = base64.b64decode(signature, validate=True)
print("decoded signature length:", len(decoded_signature))
except (binascii.Error, ValueError):
print("signature is not valid Base64")
如果这一步失败,说明签名在进入 RSA 验证前已经损坏。
5. 确认签名模式和接入模式一致
支付宝配置中常见的签名类型包括:
RSA:通常使用 SHA-1。RSA2:通常使用 SHA-256。
需要核对回调中的 sign_type、开放平台应用配置和 SDK 初始化配置是否一致。在确认签名类型前,不要自行对 result 计算 SHA-1 或 SHA-256。
如果应用使用证书模式,还要确认程序加载了对应证书中的支付宝公钥,并正确处理证书序列号等参数。普通公钥模式的配置不能直接用于证书模式。
6. 检查字符编码和参数类型
rsa.verify() 最终验证的是字节序列。待验签内容应使用接口约定的字符集编码。支付宝接口通常使用 UTF-8,但仍要以实际请求中的 charset 和当前 SDK 的约定为准。
message_bytes = message.encode("utf-8")
还要确认封装函数内部是否已经执行编码。如果函数需要 str,不要提前将字节错误地转换成字符串:
# 错误风险:这得到的是类似 "b'...'" 的文本,不是原始内容
message = str(original_bytes)
正确方式是:
message = original_bytes.decode("utf-8")
7. 捕获验签异常,避免回调接口直接返回 500
当前调用链没有捕获 VerificationError,异常会一直传播到 Sanic 请求处理层。验签失败是可预期的安全校验结果,应明确处理:
from rsa.pkcs1 import VerificationError
try:
verified = Alipay.verify_with_rsa(result, signature)
except VerificationError:
verified = False
except (ValueError, TypeError) as exc:
verified = False
# 记录异常类型和必要的脱敏上下文
logger.warning("Invalid Alipay signature data: %s", type(exc).__name__)
if not verified:
logger.warning("Alipay callback signature verification failed")
return text("failure", status=400)
验签异常后不能继续更新订单状态。只有验签成功且业务字段校验通过,才能处理支付结果。
推荐排查顺序
建议按以下顺序定位问题:
- 确认回调来自沙箱环境还是生产环境。
- 确认
app_id与当前应用一致。 - 确认程序加载了该应用、该环境对应的支付宝公钥。
- 确认没有混用普通公钥模式和证书模式。
- 保存一份脱敏后的原始请求信息,不要只检查解析后的对象。
- 检查
signature的长度、空格、Base64 合法性,以及是否被重复解码。 - 确认待验签参数已排除
sign和sign_type。 - 确认参数排序、拼接和字符编码符合当前 SDK 的要求。
- 对比传入
verify_with_rsa()的最终字符串与支付宝实际签名的内容。 - 使用同一组固定参数和签名在本地复现,排除请求框架和中间代理的影响。
验签成功后仍需进行业务校验
RSA 验签成功只能证明回调内容没有被篡改,不能单独证明该通知属于当前订单。至少还要核对以下字段:
if params["app_id"] != EXPECTED_APP_ID:
return text("failure", status=400)
if params["out_trade_no"] != expected_order_no:
return text("failure", status=400)
if params["seller_id"] != EXPECTED_SELLER_ID:
return text("failure", status=400)
if Decimal(params["total_amount"]) != expected_amount:
return text("failure", status=400)
订单处理还应具备幂等性,避免支付宝重复通知导致重复发货、重复充值或重复记账。验签和业务校验全部通过后,再按照支付宝回调协议返回成功响应。
备注:内容仅供参考。
