导读:本期聚焦于不吃香菜创作的《微信公众号支付退款结果解密详解:Java实现退款通知解密的完整示例代码》,敬请观看详情。微信支付退款结果通知里的req_info字段是加密串,直接读取拿不到退款单号和退款状态,必须用商户API证书密钥做AES-256-GCM解密才能拿到明文。这篇文章围绕Java实现微信退款结果解密展开,讲清楚解密的完整流程:先对API密钥做MD5得到解密密钥,再对密文做Base64解码,最后用AES/GCM/NoPadding完成解密并解析XML。文中给出可直接运行的完整代码,包含工具类封装、GCM参数处理、常见异常排查(BadTagException、密钥长度不对等),并附上回调接口的验签与解密衔接写法,帮助开发者少踩坑。

做微信公众号支付退款功能的开发者基本都会遇到同一个问题:退款结果通知推送过来的数据里,refund_status、refund_request_source这些关键信息全被塞在一个叫req_info的加密字段里,直接打印出来是一串Base64乱码,根本没法用。这是因为微信出于安全考虑,对退款结果通知做了单独加密处理,加密方式是AES-256-GCM,解密密钥也不是直接用商户API密钥,而是要先做一次MD5。不少人在这一步栽了跟头,要么解密时抛出BadTagException,要么解出来是乱码。本文把整个解密流程和完整Java代码讲透,照着写就能跑通。

微信公众号支付退款结果解密详解:Java实现退款通知解密的完整示例代码

退款结果解密的整体流程与原理

先明确一点:微信支付有两类通知需要区分对待。普通支付结果通知(event类型为TRANSACTION)从v3版本开始用的是RSA加密的平台证书解密,而退款结果通知走的却是另一套完全不同的机制——即使你用的是微信支付APIv3,退款成功通知中的resource字段同样采用AEAD_AES_256_GCM算法,但密钥来源是APIv3密钥;如果是v2的退款结果通知,则是对req_info字段用商户API密钥的MD5值作为AES-256-ECB... 不对,这里要特别纠正一个常见误区:v2退款通知用的确实是AES-256-ECB模式,但微信官方文档里退款通知(无论v2接口还是v3的退款结果通知的resource解密)推荐和主流实现都是AES/GCM。以v2退款结果通知为例,官方规定的解密步骤是三步。

第一步,对商户API密钥(就是你在微信商户平台设置的32位API密钥,即md5加密方式的key)做MD5运算,得到一个32字符的小写字符串,这个字符串才是真正用于AES解密的密钥。注意MD5结果必须转成小写十六进制字符串后再使用,直接拿字节数组去初始化SecretKey会报InvalidKeyException,提示密钥长度不合法。这一步是坑最多的地方,因为很多人误以为直接拿API密钥去解密。

第二步,对通知报文中的req_info字段做Base64解码,得到AES加密的原始密文字节数组。第三步,使用AES-256-ECB模式(PKCS5Padding填充)配合第一步得到的MD5字符串作为密钥解密。这里再强调一次版本差异:v2退款通知用ECB模式,v3的退款通知resource解密用GCM模式且需要处理nonce和associated_data。下文代码两种都给出,以v2的ECB场景为主线讲解,最后补充v3的GCM写法。

Java完整解密代码实现(v2退款通知)

下面给出完整可运行的工具类,依赖方面只需要JDK自带的javax.crypto,不需要额外引包。代码里对每一步都做了封装,方便直接嵌入到Spring Boot的回调Controller里。

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.Base64;

public class WxRefundDecryptUtil {

    /**
     * 解密微信退款结果通知的req_info字段
     * @param reqInfo   通知中的加密字段(Base64字符串)
     * @param apiKey    商户平台设置的API密钥(32位)
     * @return 解密后的XML明文
     */
    public static String decryptRefundInfo(String reqInfo, String apiKey) throws Exception {
        // 第一步:对API密钥做MD5,得到小写十六进制字符串作为解密密钥
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        byte[] digest = md5.digest(apiKey.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte b : digest) {
            String hex = Integer.toHexString(b & 0xFF);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        String md5Key = sb.toString(); // 32位小写密钥

        // 第二步:Base64解码密文
        byte[] cipherBytes = Base64.getDecoder().decode(reqInfo);

        // 第三步:AES-256-ECB解密,PKCS5Padding填充
        SecretKeySpec keySpec = new SecretKeySpec(md5Key.getBytes("UTF-8"), "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, keySpec);
        byte[] plainBytes = cipher.doFinal(cipherBytes);

        return new String(plainBytes, "UTF-8");
    }

    public static void main(String[] args) throws Exception {
        String apiKey = "你的32位商户API密钥";
        String reqInfo = "通知报文里拿到的req_info字段值";
        String xml = decryptRefundInfo(reqInfo, apiKey);
        System.out.println(xml);
    }
}

解密成功后返回的是一段XML字符串,结构大致包含out_refund_no(商户退款单号)、refund_status(退款状态,SUCCESS表示成功)、refund_recv_account(退款入账账户)、settlement_refund_fee(退款金额)等字段。解析XML可以用dom4j,也可以简单点用正则或者Hutool的XmlUtil,看个人项目习惯。

这里提两个容易出错的处理细节。其一,取req_info之前一定要先把整个回调XML解析出来,注意微信回调报文是XML不是JSON,用JSON库去解析会直接失败。其二,如果部署环境是JDK 8早期版本,默认的JCE策略文件限制了AES密钥最长128位,而MD5后的32字节密钥对应256位,会抛出InvalidKeyException: Illegal key size。解决办法是升级到JDK 8u161以上版本,或者手动替换local_policy.jar和US_export_policy.jar两个策略文件。

v3退款通知的GCM解密写法

如果你的项目用的是APIv3,退款成功通知里的resource字段解密逻辑不太一样。GCM模式需要额外的nonce(随机串)和认证标签,Java里通过GCMParameterSpec指定认证标签位数,通常为128位。密钥直接使用APIv3密钥(32字节字符串),不需要再做MD5。

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class WxV3RefundDecryptUtil {

    public static String decryptResource(String ciphertext, String nonce, String apiV3Key) throws Exception {
        byte[] keyBytes = apiV3Key.getBytes(StandardCharsets.UTF_8);
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");

        // GCM模式,认证标签长度128位
        GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8));
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, keySpec, spec);

        byte[] result = cipher.doFinal(Base64.getDecoder().decode(ciphertext));
        return new String(result, StandardCharsets.UTF_8); // 返回JSON明文
    }
}

v3解出来的明文是JSON格式,退款状态字段是refund_status,退款单号是out_refund_no,金额字段包含payer_refund、amount等嵌套结构,用Jackson或Gson反序列化即可。GCM解密如果抛出BadTagException,几乎可以断定是密钥不对、nonce取错了字段,或者密文在传输中被截断(比如URL编码处理时出了问题),优先检查这三项。

回调接口的完整衔接与常见异常排查

解密只是回调处理的一环,一个健壮的回调接口顺序应该是:先验签,再解密,然后业务处理,最后按微信要求的格式返回应答。v2回调验签用API密钥对通知内容做MD5签名比对,v3回调验签用微信支付平台证书验证Wechatpay-Signature请求头。验签通过后才进行解密,这个顺序不能反,否则你的接口等于对外暴露了一个免费的解密服务。

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/wx/notify")
public class WxRefundNotifyController {

    @PostMapping(value = "/refund", produces = "text/xml;charset=UTF-8")
    public String refundNotify(@RequestBody String xmlBody) {
        try {
            // 1. 解析XML拿到req_info(此处省略解析,假设已拿到)
            String reqInfo = extractReqInfo(xmlBody);

            // 2. 验签(省略,务必实现)
            // 3. 解密
            String plainXml = WxRefundDecryptUtil.decryptRefundInfo(reqInfo, "你的API密钥");

            // 4. 业务处理:更新退款单状态等
            System.out.println("退款结果明文:" + plainXml);

            // 5. 返回成功应答,微信收到后停止重推
            return "<xml><return_code><![CDATA[SUCCESS]]></return_code>"
                 + "<return_msg><![CDATA[OK]]></return_msg></xml>";
        } catch (Exception e) {
            // 返回失败,微信会按衰减策略重试推送
            return "<xml><return_code><![CDATA[FAIL]]></return_code>"
                 + "<return_msg><![CDATA[解密失败]]></return_msg></xml>";
        }
    }
}

关于异常排查,再补充几条实战经验。第一,解密结果开头不是<xml>而是一堆乱码,说明密钥对了但填充模式不对,检查是否误用了NoPadding。第二,报Input length must be multiple of 16,通常是Base64解码环节出了问题,确认req_info没有被二次编码或截断。第三,本地测试通过、线上失败,重点排查线上环境JDK的密钥长度限制以及API密钥是否与商户号匹配——一个商户号对应一套密钥,用错商户的密钥永远解不出来。第四,微信的退款通知会重复推送,业务代码里要做幂等处理,建议以out_refund_no加唯一索引兜底,避免重复更新退款状态。

最后提醒一点安全规范:API密钥和APIv3密钥不要硬编码在代码里,应该放到配置中心或环境变量中,日志里也绝对不要打印解密后的完整明文,退款入账账户等信息属于敏感数据。把解密工具类、验签逻辑、业务处理分层封装,后续微信切换通知格式时改动成本也最小。按本文的代码和排查思路走下来,退款结果解密这个环节基本不会留下隐患。

微信支付退款解密Java解密AES-256-GCM退款结果通知修改时间:2026-09-14 05:00:50

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