尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
微信支付v3退款接口实战:Spring Boot集成与AES-GCM解密
简介面向Java后端开发者的微信退款接口对接示例包解决商户处理用户退款请求时与微信服务器安全交互的完整流程适合需要快速接入微信支付退款功能的电商、ERP及第三方支付系统开发者。压缩包共29个文件大小1.92MB其中包含10个jar依赖库、6个Java源码与6个编译后的class文件另有XML配置、工程描述文件等构成可直接运行的示例工程便于从代码层面理解加载PKCS12证书、创建SSLContext、配置安全连接、构造退款参数并携带数字签名发送POST请求的实现细节。已有870人学习下载。testWeixinRefund类演示了从证书加载到响应解析的关键步骤涵盖HTTPS证书管理、RSA2048签名算法、JSON数据组装、超时设置与错误处理等核心知识点开发者可在此基础上直接替换商户号、订单号、退款金额等参数快速集成到自身项目中减少微信退款接口对接中的踩坑成本。1. 微信退款接口让很多后端头疼不是没有原因的做过支付单的人多半有这种感触支付回调调通之后刚松了口气一到退款环节证书、签名、解密、金额单位各种问题一起冒出来。这个 Java 项目做的就是把微信支付 v3 退款从发起到回调的整条链路拆开揉碎以 Spring Boot 工程为载体覆盖退款请求、同步结果解析、异步通知验签、AES-GCM 解密和退款单状态落库这几个核心动作最终对外暴露一个普通 HTTP 接口。适合正在对接微信支付退款的 Java 后端也适合想彻底看懂 v3 签名和回调机制、准备把退款模块做二次改造的开发者在本地拉起来跑一遍。项目本身不复杂但涉及的坑非常密集值得完整过一遍。2. 技术选型与整体链路退款单在微信支付 v3 里的完整路径微信支付接口从 v2 换到 v3 之后最直观的变化是报文格式从 XML 变成了 JSON签名方式也从 MD5 升级成了 SHA256-RSA。很多老代码里的退款实现是基于 v2 的挪到 v3 上几乎要重写签名和回调验签部分。这个项目直接基于 v3 搭建用 Spring Boot 做 Web 层HTTP 客户端用的是 OkHttpJSON 处理用的是 Jackson加解密单独抽了一个 crypto 包没有把逻辑堆在 Controller 里。这样设计的好处是其他项目想复用退款能力时可以直接拷贝 service 和 crypto 两个包不用带着 Web 层一起走。2.1 退款和支付不是同一套逻辑支付接口通常只需要商户私钥签名请求而退款接口对安全性的要求明显更高因为它涉及资金的逆向操作。v3 退款请求除了要带商户私钥生成的 Authorization 头还必须携带商户证书序列号接收微信异步通知时又要用平台证书去验证响应签名再用 APIv3 密钥解密回调里的 resource 字段。退款请求的入参也比支付复杂一些。实际项目中经常被忽略的几个字段如下字段含义是否必填备注out_trade_no商户原支付订单号二选一与 transaction_id 二选一transaction_id微信支付订单号二选一更推荐使用这个out_refund_no商户退款单号必填同一商户下单号不能重复amount.refund退款金额必填单位是分amount.total原订单金额必填单位是分amount.currency币种必填默认 CNYnotify_url异步回调地址选填不传则用商户后台配置reason退款原因选填部分退款时建议传方便运营对账这里最容易踩的坑是金额单位。数据库里如果存的是元组装请求前必须转成分为单位的 int否则退款金额会放大一百倍。项目里专门写了一个 MoneyUtils 类处理这个转换后面章节会贴出来。2.2 工程目录设计与依赖选型项目结构保持了一个后端服务最基本的形态但把微信相关的代码单独隔离了出来。核心结构如下src/main/java/com/example/refund ├── config │ └── WechatPayConfig.java ├── controller │ └── RefundController.java ├── service │ ├── RefundService.java │ └── WechatRefundService.java ├── model │ ├── RefundRequest.java │ ├── RefundResponse.java │ └── RefundNotifyResult.java ├── crypto │ ├── SignatureHelper.java │ └── AesGcmDecryptor.java └── repository └── RefundOrderMapper.java依赖选择上有一个取舍微信官方提供 wechatpay-java SDK能省掉签名头的构造和平台证书管理但也会把很多实现细节包起来。这个项目没有完全依赖 SDK而是自己实现了 SignatureHelper 和 AesGcmDecryptor原因有两点一是退款回调里的 AES-GCM 解密逻辑在排错时特别需要能单步调试二是自己实现一遍对 v3 签名的构造规则理解会深很多。实战中我一般建议别人第一次对接时用 SDK等对签名机制熟悉之后再自己做封装二次开发时更容易按业务需求裁剪。2.3 一条退款请求从发起到完成的生命周期这个链路在项目里可以分为七个步骤理解了这七步后面的代码看起来就不费劲。第一步业务系统收到退款申请Controller 先做参数校验确认退款金额不超过原订单可用余额。第二步组装退款单号 out_refund_no这个单号必须全局唯一幂等性全靠它。第三步用商户私钥对请求方法、请求路径、时间戳、随机串、请求体拼接出的字符串做 RSA-SHA256 签名生成 Authorization 头。第四步向微信支付退款接口发起 HTTPS POST 请求。第五步同步拿到退款单初始状态通常是 PROCESSING本地落库。第六步微信支付异步通知退款结果项目收到通知后先验签再解密 resource 字段拿到退款成功或失败的状态。第七步对比本地退款单和微信推送的状态更新数据库并触发后续业务逻辑。这七步里第四步和第六步是失败率最高的地方。可能出现 401 签名错误、证书序列号不存在、回调验签失败、解密报错等一堆问题这些问题几乎都集中在签名、证书、密钥和字段格式上后面章节会逐条写清楚。3. 核心代码拆解签名、HTTP 请求和回调解密这一章把项目里最核心的三个代码片段拿出来逐段讲。先是签名生成。v3 签名不是一个简单的对请求体做加密而是要把办法、请求路径、时间戳、随机字符串、请求体拼成一个带换行符的字符串再用商户私钥做 SHA256withRSA 签名。3.1 用商户私钥生成 Authorization 头SignatureHelper 里最核心的方法如下private String buildSignMessage(String method, String urlPath, String body, String timestamp, String nonce) { return method \n urlPath \n timestamp \n nonce \n body \n; } private String sign(String message, PrivateKey privateKey) throws Exception { Signature signature Signature.getInstance(SHA256withRSA); signature.initSign(privateKey); signature.update(message.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(signature.sign()); }buildSignMessage 方法是整个签名机制的骨架换行符顺序必须完全一致少一个换行或者多一个都会导致验签失败。method 是实际请求的 HTTP 方法退款接口固定是 POSTurlPath 是请求路径必须包含/v3/refund/domestic/refunds不能带域名body 是请求体的原始字符串不能做格式化如果使用 Jackson 序列化对象后还要按原字符串参与签名注意不要重新排序字段最好用 LinkedHashMap 保证字段顺序稳定。拿到签名值之后Authorization 头的格式是固定的String authHeader WECHATPAY2-SHA256-RSA2048 mchid\ mchId \, nonce_str\ nonce \, signature\ signature \, timestamp\ timestamp \, serial_no\ serialNo \;这里的 serial_no 是商户证书的序列号不是证书文件里的文件名。很多人在这一步把证书公钥文件本身的备注当序列号用导致微信后台找不到对应的商户证书。serial_no 应该通过解析 apiclient_cert.pem 获取代码里可以用证书工厂读取 Certificate 对象再调 getSerialNumber()。3.2 组装退款请求并发送项目里用 OkHttp 发送退款请求核心逻辑集中在 WechatRefundServiceprivate String sendRefundRequest(RefundRequest refundRequest) { String path /v3/refund/domestic/refunds; String timestamp String.valueOf(System.currentTimeMillis() / 1000); String nonce UUID.randomUUID().toString().replace(-, ); String body objectMapper.writeValueAsString(refundRequest); String message buildSignMessage(POST, path, body, timestamp, nonce); String signature sign(message, privateKey); String authHeader buildAuthHeader(timestamp, nonce, signature); String endpoint https://api.mch.weixin.qq.com path; Request request new Request.Builder() .url(endpoint) .header(Authorization, authHeader) .header(Content-Type, application/json) .header(Accept, application/json) .post(RequestBody.create(body, JSON)) .build(); try (Response response httpClient.newCall(request).execute()) { return response.body().string(); } }time stamp 必须是秒级不能用毫秒nonce 建议用 32 位以内的随机字符串body 字符串两部分都用到既作为 HTTP 请求体发送又参与签名计算。Content-Type 必须是 application/jsonAccept 也建议显式声明否则微信网关可能按默认文本类型处理。同步响应返回的 JSON 里能拿到 refund_id、out_refund_no、status、amount 等字段但注意这个同步响应不代表退款已经成功。status 为 PROCESSING 时还需要等异步通知这一点很多人第一次对接时容易误解以为同步响应成功就等于退款成功实际上微信支付对退款是异步确认资金状态的。3.3 退款结果回调解密微信退款回调的请求体结构固定是{ id: EV-TEST, event_type: REFUND.SUCCESS, resource_type: encrypt-resource, resource: { algorithm: AEAD_AES_256_GCM, ciphertext: base64..., associated_data: refund, nonce: 加密随机串, original_type: transaction } }resource 里的 ciphertext 是 AES-256-GCM 加密后的退款结果 JSON。解密代码在 AesGcmDecryptor 里private String decrypt(String ciphertext, String nonce, String associatedData, String apiV3Key) throws Exception { Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); SecretKeySpec keySpec new SecretKeySpec( apiV3Key.getBytes(StandardCharsets.UTF_8), AES); GCMParameterSpec gcmSpec new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8)); cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec); cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8)); byte[] plaintext cipher.doFinal( Base64.getDecoder().decode(ciphertext)); return new String(plaintext, StandardCharsets.UTF_8); }解密时 associatedData 必须原样传入不能省略否则 GCM 模式会直接报 AEADBadTagException。apiV3Key 是商户平台的 APIv3 密钥长度固定为 32 个字节解密使用的密钥长度不是 GCMParameterSpec 里的 128128 指定的是认证标签长度。项目里把解密结果反序列化成 RefundNotifyResult里面包含 out_refund_no、refund_status、success_time 等字段再拿 out_refund_no 去 update 本地退款单。不要忽略对回调的验签。解密之前必须先用平台证书验证微信返回的 Wechatpay-Signature 头如果跳过验签直接解密请求来源无法确认。验签逻辑和解密逻辑类似也是拼接时间戳、随机串、原始响应体但使用的是微信支付平台证书的公钥而不是商户证书。4. 环境配置与避坑记录本地跑通退款接口的关键细节光有代码还不够微信退款接口的联调环境配置本身就有很多讲究。最常见的问题是证书加载失败、序列号对不上、回调收不到。这一章先讲标准的配置方式再讲几个高频踩坑点都是我实际排查中遇到过的。4.1 证书、密钥和参数配置项目里用 application.yml 承载微信支付相关配置配置项如下wechat: mch-id: 1900000109 api-v3-key: 32字节的APIv3密钥 private-key-path: classpath:/certs/apiclient_key.pem private-cert-path: classpath:/certs/apiclient_cert.pem platform-cert-path: classpath:/certs/wechatpay_platform_cert.pem notify-url: https://api.example.com/api/refund/notifymch-id 是商户号api-v3-key 在商户平台的 API 安全里设置private-key-path 对应商户私钥文件用来生成退款请求签名platform-cert-path 对应微信支付平台证书用来验证回调和解密。注意微信支付平台证书和商户证书不是同一个文件平台证书是微信支付侧的公钥证书商户证书是自己的。二者千万不要混用。项目里在 WechatPayConfig 中做了一次初始化加载Bean public PrivateKey privateKey() throws Exception { try (InputStream in resourceLoader.getResource(privateKeyPath).getInputStream()) { String pem new String(in.readAllBytes(), StandardCharsets.UTF_8); String base64 pem.replace(-----BEGIN PRIVATE KEY-----, ) .replace(-----END PRIVATE KEY-----, ) .replaceAll(\\s, ); byte[] keyBytes Base64.getDecoder().decode(base64); return KeyFactory.getInstance(RSA).generatePrivate( new PKCS8EncodedKeySpec(keyBytes)); } }项目放在 classpath 下的证书文件Jar 打包后要用 classpath 方案读取不要用 File 路径。如果以后把证书放到配置中心或 OSS改成 resourceLoader 的 URL 前缀即可不影响上层调用。4.2 本地联调别直接打正式环境退款是资金敏感操作正式环境里的退款金额是真金白银。微信支付不像有的支付平台提供完整沙箱环境退款接口的实际联调通常有几种替代方案一是使用商户测试号加小额真实退款二是在本地用 Mock 服务模拟退款请求和回调结果。项目里预留了一个本地模拟回调的方式就是给 RefundController 加一个测试接口手动把一个 Mock 的加密 resource 报文推给 notify 接口用真实代码跑一遍验签和 AES-GCM 解密流程。这样可以在不依赖微信回调的前提下验证本地逻辑。做这个模拟时需要生成一个临时的平台证书和密钥对保证验签能通过。这个思路在二次开发时非常有用避免每次联调都要等微信服务器回调开发效率能提升不少。4.3 高频踩坑记录现象一退款接口返回 401错误信息提示签名错误但签名代码反复检查过看起来没问题。原因大概率是 Authorization 头里的 serial_no 和签名私钥不是同一对。很多项目把商户证书序列号和私钥分开存放在两个配置项里上线时手动替换证书只换了私钥或者只换了证书两边不一致。解决不要手工维护 serial_no直接从 apiclient_cert.pem 解析出来和 privateKey 保证来自同一个证书文件。这样即使证书轮换代码也能自动保持一致。现象二回调通知验签一直失败请求体明明没动过。原因验签时用了自己商户的 apiclient_cert.pem 公钥。微信回调的 Wechatpay-Signature 签名主体是微信支付平台证书不是商户证书。平台证书要从微信支付官方渠道下载并在证书有效期到期前主动更新。解决验签和加解密统一使用平台证书发起退款请求才使用商户证书两者职责分开。现象三解密报 AEADBadTagException抛 IllegalBlockSizeException 或者标签校验失败。原因APIv3 密钥配错或者传入的 associated_data 为空或者 nonce 被当作 Base64 解码了。注意回调里的 nonce 是明文字符串不需要 Base64 解码直接取 UTF-8 字节即可。解决确认 api-v3-key 是商户平台里设置的原始字符串不能做 MD5 处理也不能转成 Hex。ciphertext 做 Base64 解码nonce 直接 getBytes 使用associatedData 原样透传。现象四同一笔退款收到两次回调通知业务表里出现两条处理记录退款单重复处理。原因微信支付为了确保通知送达会多次重推相同事件。项目里只用 out_trade_no 做关联没有做退款单唯一约束。解决在本地退款单表对 out_refund_no 建唯一索引处理回调时先检查本地状态已成功的直接返回成功响应。幂等逻辑如果放在应用层判断并发下仍然有可能重复处理数据库唯一索引才是兜底。5. 二次开发改造从写死流程到一个可复用的退款模块这个项目跑通之后最值得做的事就是把微信退款实现从业务代码里抽出来封装成一个可以替换实现的退款通道。我这里给出一条比较落地的改造路径。第一步把退款操作提炼成接口业务侧不依赖微信 SDK 的具体实现。接口设计得尽量薄只需要三个能力发起退款、主动查询退款、接收回调通知。public interface RefundChannel { RefundResult create(RefundCommand cmd); RefundResult query(String outRefundNo); RefundResult handleNotify(String body); }微信退款作为 RefundChannel 的默认实现持有一个 RefundChannel 的 Spring Bean后续如果切换其他支付渠道只需要新增一个实现类。第二步引入本地退款单状态机。我一般会在退款单表里增加 status 字段初始值为 PROCESSING微信同步响应返回 PROCESSING 后保持不变回调通知里如果 refund_status 是 SUCCESS则改为 SUCCESS如果是 CLOSED 则改为 FAILED。这样业务方只看本地状态不直接面对微信的语义。第三步把主动查询做成兜底任务。微信回调不是绝对可靠的虽然会重试但长时间未收到回调控件退款单可能一直停在 PROCESSING。我习惯写一个定时任务把超过两分钟仍处于 PROCESSING 的退款单拿出来调 RefundChannel 的 query 接口主动确认状态。这个兜底逻辑在实际项目中比想象中更重要尤其是回调地址网络抖动的时候。改造完之后整个模块跟支付主流程几乎完全解耦业务方只需要关心退款单号、金额和状态。之前在给一个多商户项目做退款能力时因为只想着把微信接口调通疏忽了退款单表唯一索引结果同一笔订单的并发退款请求直接把业务数据打出了两条重复记录微信侧靠 out_refund_no 挡住了重复退款本地业务账却对不上了。那之后我每次接支付或者退款强制第一步先画状态机第二步把唯一索引建好最后才碰 HTTP 接口。希望这个项目能帮你把微信退款这条路一次走顺。本文还有配套的精品资源点击获取
RELATED

相关推荐

信号相位冲突0.01秒仲裁:Java在交通控制中的高实时实践

信号相位冲突0.01秒仲裁:Java在交通控制中的高实时实践

1. 路口让灯“打架”的真相:相位冲突到底从哪来1.1 信号灯不是“单灯控制”,而是一组相位在轮转先说一个经常被忽略的基本概念:路口信号机并不是在单独控制每一根灯杆,而是在按“相位”工作。所谓相位,是指在一个路口&…

📅 2026/10/16 13:24:29
Phi-4-mini-instruct_rai_1.8.0_npu_16K 合规与安全:MIT 许可证、AMD 修改条款与第三方依赖清单

Phi-4-mini-instruct_rai_1.8.0_npu_16K 合规与安全:MIT 许可证、AMD 修改条款与第三方依赖清单

【免费下载链接】Phi-4-mini-instruct_rai_1.8.0_npu_16K 项目地址: https://ai.gitcode.com/hf_mirrors/amd/Phi-4-mini-instruct_rai_1.8.0_npu_16K 点击查看 免费下载 本文带你快速看懂 Phi-4-mini-instruct_rai_1.8.0_npu_16K 的许可证与合规要点:这…

📅 2026/10/16 13:24:29
代码审查成了 AI 最卷的赛道:108 亿独角兽刚诞生,阿里开源 agent 就冲上热榜

代码审查成了 AI 最卷的赛道:108 亿独角兽刚诞生,阿里开源 agent 就冲上热榜

代码审查成了 AI 最卷的赛道:108 亿独角兽刚诞生,阿里开源 agent 就冲上热榜 【免费下载链接】open-code-review Secure, fast, efficient, battle-tested at Alibabas scale. Hybrid architecture code review tool: deterministic pipelines LLM Agen…

📅 2026/10/16 13:24:29
MORE NEWS

更多资讯

📰

Linux审计事件实时分发:audispd原理与高可靠管道实践

1. 项目概述:audit日志的“管道工”——audispd到底在干啥你有没有遇到过这种场景:Linux系统里启用了auditd,/var/log/audit/audit.log里日志哗哗地写,但你想实时抓取其中特定事件——比如某个进程调用了execve、某用户修改了/etc…

📰

Qt调用MATLAB生成DLL实战:跨平台算法集成方案

1. 项目概述:为什么Qt要和MATLAB生成的DLL打交道在工业软件、科研工具链和嵌入式上位机开发中,我经常遇到一个现实矛盾:Qt写界面快、跨平台稳、生态成熟,但做复杂数学建模、信号处理、图像识别或控制系统仿真时,硬啃C数…

📰

LEAP-1A26发动机性能建模实战:数据准备、稳态匹配与调参避坑

简介:面向飞行模拟与航空发动机建模开发者,这份资料围绕CFM LEAP-1A26发动机及FADEC控制展开,聚焦物理/理论建模与参数分析。包内共114个文件,以Python脚本(25个py)、图表(14个png)、…

📰

Starrc寄生提取与后仿偏差归因实战指南

简介:本资源是一份面向IC后端设计工程师与高校微电子专业学习者的StarRC寄生参数提取与后仿真全流程实践指南,聚焦物理验证后关键的寄生建模与电路性能闭环验证环节。文档以SMIC 40nm工艺库为背景,系统梳理了从LVS规则修改、CCI接口配置、Cal…

📰

计算机考研复试英语口语:高频题型与答题框架全攻略

最近有好几位准备考研复试的学弟学妹来找我,问的都是同一个问题:计算机与软件工程专业的研究生复试英语口语,到底会考什么?很多人准备了厚厚一叠自我介绍,对着镜子背得滚瓜烂熟,结果我一问“你毕设用到的那…

📰

版本地狱实录:Wine、FEX-Emu、DXMT 三方版本不匹配就是一场白折腾

版本地狱实录:Wine、FEX-Emu、DXMT 三方版本不匹配就是一场白折腾 【免费下载链接】Madeira Run x86-64 Windows PC games on jailed iOS via FEX-Emu Wine DXMT 项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira 在 Apple Silicon 上跑 Window…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬