导读:本期聚焦于灯下变量创作的《微信公众号支付退款结果通知签名验证怎么做?Node.js实现签名验证完整代码示例》,敬请观看详情。微信支付退款结果通知采用特殊的双重安全机制,与普通支付通知不同,它除了需要对通知报文做签名验证外,还需要用商户API证书密钥对resource字段进行AES-256-GCM解密才能拿到退款详情。本文围绕这一流程展开,先分析v3版本退款通知的报文结构和验签原理,再给出Node.js平台下从下载平台证书、验证SHA256-RSA签名到解密refund内容的完整代码实现,并附上返回应答报文、处理重复通知等实战细节,帮助开发者避开验签失败和解密异常等常见坑。

微信支付的退款结果通知是回调体系里比较特殊的一类。它不像普通支付成功通知那样直接把业务数据明文放在报文里,而是把退款详情加密后放在resource字段中,开发者需要先用微信支付平台证书验证整个报文的签名,再用商户API证书对应的密钥对resource做AES-256-GCM解密。不少人在处理退款通知时验签总是失败,或者解密时抛出异常,根源往往是对这套双重机制理解不到位。本文以v3版本接口为例,完整讲解Node.js环境下的验签与解密实现。

微信公众号支付退款结果通知签名验证怎么做?Node.js实现签名验证完整代码示例

一、退款结果通知的报文结构与验签原理

微信支付在退款状态变化时,会向商户配置的回调地址推送POST请求,报文主体是JSON格式。典型的退款通知结构如下:

{
  "id": "EV-2018022511223320873",
  "event_type": "REFUND.SUCCESS",
  "resource_type": "encrypt-resource",
  "summary": "退款成功通知",
  "resource": {
    "algorithm": "AEAD_AES_256_GCM",
    "ciphertext": "基base64编码的密文",
    "associated_data": "transaction",
    "original_type": "refund",
    "nonce": "随机字符串"
  }
}

HTTP请求头中包含三个关键字段:Wechatpay-Timestamp(时间戳)、Wechatpay-Nonce(随机串)和Wechatpay-Signature(Base64编码的签名)。验签的核心逻辑是:把请求体原文、时间戳、随机串按固定顺序拼接成一个字符串,再用微信支付平台证书的公钥做SHA256-RSA验签。这里有个非常容易踩的坑——参与验签的必须是请求体的原始字节,如果你的Web框架在到达业务代码之前做了任何字符串转换(比如编码转换、JSON解析后再序列化),拼接出来的内容就与签名时的原文不一致,验签必然失败。

另外要注意区分平台证书和商户API证书这两个概念。商户API证书是你自己申请的,用于请求微信接口时签名;平台证书是微信官方的,用于验证微信发来的通知。平台证书可以通过微信的证书下载接口获取,也可以使用微信提供的公钥模式。生产环境建议定期刷新并缓存证书,避免证书轮换后验签突然失败。

二、Node.js验签与解密完整代码实现

下面给出一段可直接运行的完整代码,基于Express框架和Node.js内置的crypto模块,不依赖第三方SDK。首先是接收原始请求体的配置:

const express = require('express');
const crypto = require('crypto');

const app = express();

// 必须用raw类型接收请求体,保证验签使用的是原始字节
app.use(express.raw({ type: '*/*', limit: '2mb' }));

// 平台证书公钥(PEM格式),生产环境应从证书下载接口动态获取
const PLAToFORM_PUBLIC_KEY = `-----BEGIN CERTIFICATE-----
MIIDxxxxx...(平台证书内容)
-----END CERTIFICATE-----`;

// 商户API证书对应的密钥v3密钥,32字节字符串
const API_V3_KEY = '你的32位APIv3密钥';

// 验证签名
function verifySignature(body, timestamp, nonce, signature) {
  // 按官方规则拼接待验签字符串
  const message = `${timestamp}\n${nonce}\n${body}\n`;
  const signatureBuffer = Buffer.from(signature, 'base64');
  return crypto.createVerify('RSA-SHA256')
    .update(message, 'utf8')
    .verify(PLATFORM_PUBLIC_KEY, signatureBuffer);
}

// AES-256-GCM解密resource
function decryptResource(nonce, associatedData, ciphertext) {
  const buf = Buffer.from(ciphertext, 'base64');
  const authTag = buf.subarray(buf.length - 16);   // 最后16字节是认证标签
  const data = buf.subarray(0, buf.length - 16);   // 前面部分是密文
  const decipher = crypto.createDecipheriv('aes-256-gcm', API_V3_KEY, nonce);
  decipher.setAuthTag(authTag);
  if (associatedData) decipher.setAAD(Buffer.from(associatedData));
  return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
}

上面的代码有两个关键点需要展开说明。第一是验签字符串的拼接格式,三部分之间用换行符\n分隔,且末尾也有一个换行符,这是官方文档明确规定但经常被忽略的细节。第二是GCM解密时认证标签的提取:密文Base64解码后,最后16个字节是authTag,其余才是真正的密文数据。如果用setAuthTag时传入了错误长度的标签,会直接抛出认证失败的异常。

接下来是路由处理部分,完成验签、解密并返回应答:

app.post('/refund/notify', (req, res) => {
  const body = req.body.toString('utf8');
  const timestamp = req.get('Wechatpay-Timestamp');
  const nonce = req.get('Wechatpay-Nonce');
  const signature = req.get('Wechatpay-Signature');
  const serial = req.get('Wechatpay-Serial');

  // 1. 验证签名,serial可用于匹配多张平台证书
  if (!verifySignature(body, timestamp, nonce, signature)) {
    console.error('验签失败');
    return res.status(401).json({ code: 'FAIL', message: '验签失败' });
  }

  // 2. 解析报文并解密resource
  const notify = JSON.parse(body);
  const { ciphertext, nonce: resNonce, associated_data: aad } = notify.resource;
  const refundData = JSON.parse(
    decryptResource(resNonce, aad, ciphertext)
  );

  // 3. 业务处理:根据退款状态更新订单
  console.log('退款单号:', refundData.out_refund_no);
  console.log('退款状态:', refundData.refund_status);

  // 4. 返回成功应答,微信才会停止重推
  res.json({ code: 'SUCCESS', message: '成功' });
});

app.listen(3000, () => console.log('监听退款通知:3000'));

三、常见问题与生产环境注意事项

第一类问题是验签始终返回false。除了前面提到的请求体被修改之外,还要检查是否用错了证书——用商户证书的公钥去验签是常见错误,两者公钥并不相同。此外时间戳拼接时不要自行转成毫秒,微信传的是秒级时间戳,直接使用即可。建议同时校验时间戳与当前时间的差值(一般允许5分钟内),防止重放攻击。

第二类问题是解密抛出异常。排查方向包括:APIv3密钥是否正确(注意是商户平台设置的32位密钥,不是证书密钥文件)、nonce是否取自resource内部而不是请求头的nonce、associated_data是否正确传入。很多开发者把请求头的Wechatpay-Nonce误当成解密用的nonce,这是两个完全不同的值。

第三类是重复通知的处理。微信在未收到成功应答时会按衰减频率重试多次,业务代码必须做好幂等:解密出的退款单号out_refund_no和通知IDnotify.id都可以作为幂等键,处理前先查库判断是否已处理过。另外多实例部署时要注意证书缓存的共享与刷新,可以定时(例如每12小时)调用证书下载接口更新本地证书列表,根据请求头的Wechatpay-Serial匹配使用对应证书,这样即使微信轮换证书也不会导致线上验签故障。完成上述几点,退款通知的完整闭环就稳妥了。

微信支付退款通知Node.js签名验证AES-256-GCM解密修改时间:2026-09-01 17:02:35

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。