PAYATHON 2026

支付回调结果验签失败并抛出 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)

验签异常后不能继续更新订单状态。只有验签成功且业务字段校验通过,才能处理支付结果。

推荐排查顺序

建议按以下顺序定位问题:

  1. 确认回调来自沙箱环境还是生产环境。
  2. 确认 app_id 与当前应用一致。
  3. 确认程序加载了该应用、该环境对应的支付宝公钥。
  4. 确认没有混用普通公钥模式和证书模式。
  5. 保存一份脱敏后的原始请求信息,不要只检查解析后的对象。
  6. 检查 signature 的长度、空格、Base64 合法性,以及是否被重复解码。
  7. 确认待验签参数已排除 sign 和 sign_type。
  8. 确认参数排序、拼接和字符编码符合当前 SDK 的要求。
  9. 对比传入 verify_with_rsa() 的最终字符串与支付宝实际签名的内容。
  10. 使用同一组固定参数和签名在本地复现,排除请求框架和中间代理的影响。

验签成功后仍需进行业务校验

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)

订单处理还应具备幂等性,避免支付宝重复通知导致重复发货、重复充值或重复记账。验签和业务校验全部通过后,再按照支付宝回调协议返回成功响应。

备注:内容仅供参考。