☰
农行web端网银支付Java接口对接:证书签名验签与回调处理实战
2026/10/7 15:28:13 网站建设 项目流程

简介:一套完整的农行Web端网银支付Java接口资料包,面向需要对接农业银行在线支付能力的Java开发者,特别适用于电商平台、在线服务商及企业门户在自有系统中集成网银支付。压缩包共147个文件,大小5.1MB,文件类型覆盖54个class、19个jar依赖库、34个jsp和28个html页面,另有properties配置、cer与truststore证书文件等;其中class/jar负责支付核心逻辑与第三方依赖,jsp/html提供前端交易示例页面,properties和证书则用于环境配置与安全通信。已有2073人学习下载。通过运行附带Demo,可快速理解从初始化支付请求、组装交易参数、签名验签,到接收银行响应、处理异常以及回调通知的完整流程。资源中的升级版接口包封装了商户配置、参数解析、签名服务、数据验签等实用工具类,开发者可直接复用并在此基础上扩展新功能;对于需要快速上线农行网银支付的企业而言,这份资料能明显缩短对接调研与开发周期。

1. 农行web端网银支付Java接口:一套文档加demo能不能让你的对接少走两周弯路

农行web端网银支付Java接口文件及demo,很多人第一眼以为它只是一包接口文档拷贝。实际上这套东西里最有价值的不是PDF,而是一个能直接跑起来的Web demo:它把农行B2C网银支付从证书加载、报文签名、表单提交到回调验签整条链路串通了。对要接农行网银支付的项目来说,省下的是最开始一到两周的摸索时间。适合两类人:一是公司要接入农行网银支付、手头只有接口文档没有参考实现的Java工程师;二是想研究银行支付网关签名验签逻辑的web端开发。这篇文章我会按自己的拆包习惯,把证书、报文、签名顺序、回调处理这些关键点逐个过一遍,再把真实对接里最容易翻车的地方列出来。

2. 先把接口文档读透:证书、报文与签名验签的三层关系

拿到这套文件,我的习惯不是先开IDE,而是先把接口文档从第一页翻到支付流程那一章。农行web端网银支付这类银行接口,和互联网公司开放平台最大的差别是:报文格式极其固定,签名验签规则绕不开证书,任何一个字段顺序不对,结果就是验签失败或者银行直接拒绝请求。

2.1 目录结构与各文件定位

解压之后,文件一般会按下面几类归堆,我习惯把每个文件的功能先在脑子里标一遍:

文件位置典型文件名用途上线前是否要改
文档接口文档PDF、商户接入指南报文定义、URL、字段清单、签名算法说明以农行最新版本为准
证书merchant.pfx、bank.cer商户私钥签名、银行公钥验签换正式证书
源码PaymentServlet、NotifyServlet支付发起、回调接收demo改造成业务服务
工具类SignUtil、MerchantConfig签名验签、配置读取加固
依赖库加密jar包农行提供的加解密底层视环境保留

如果解压后没有bank.cer,只有bank_public_key.txt之类的内容,也别急,它一般是把Base64形式的公钥文本贴在配置里。核心只有两个:商户私钥用来签名,银行公钥用来验签。这两个文件对应错了,后面所有动作都会失败。

2.2 证书体系:到底谁签谁验

农行B2C网银支付用的是RSA非对称体系。商户在农行申请接入时会拿到一个pfx格式的商户证书,这个文件同时包含商户私钥和证书信息,访问pfx需要密码,密码在申请时自己设置,这就是签名用的“私钥”。农行还会提供一份银行公钥cer文件,用来验证农行发回来的回调通知确实是农行发出来的。

常见误用是把这两个角色搞反。我见过有同事拿着银行公钥去做签名,结果提交到网关直接被拒。签名的本质是私有性:商户拿自己的私钥给请求报文签名,农行用商户公钥验;反过来,农行拿自己私钥给通知报文签名,商户拿银行公钥验。所以商户证书和银行公钥的使用方向恰好相反,配置类里这两行注释一定要看清楚。

2.3 签名顺序、编码和拼接方式

农行接口文档里会给出一个“签名要素”清单。常见做法是:把商户号、订单号、金额、币种、返回地址、通知地址等字段按固定顺序拼接成一个字符串,再用商户私钥做摘要签名,最后Base64编码放进请求报文。这里的顺序不是随便排的,农行的验签程序就是按照文档里这个顺序重新拼接一遍再验。

这个环节最坑的是两点。第一,拼接时要不要带字段名、分隔符是&还是空字符串,必须和文档里的示例一致,多一个空格都会验签失败。第二,编码方式要和文档规定一致。农行这类老牌银行网关很多还沿用GBK,如果你在拼接签名原串时用UTF-8拿到字节,一旦报文里出现中文商品名,签名和对端算出来的摘要就永远对不上。测试环境里订单号全是数字可能侥幸通过,线上带中文参数就露馅。

提示:调试验签失败时,不要盯着Base64结果看。把签名前那个字符串先打印出来,和自己手拼的对比一遍,再去确认字符集。绝大多数签名问题出在拼接而不是算法。

3. 把demo跑起来:从工程导入到回调落库的完整路径

这一章说的是实际把农行web端网银支付Java接口demo跑起来的过程。这套demo通常是老式Servlet工程,结构不复杂,但运行方式和现在流行的Spring Boot不一样,环境不对容易卡在第一步。

3.1 环境准备与工程导入

我先说环境。这种银行demo往往年头不短,依赖JDK 1.7或者1.8,容器用Tomcat 7或8比较稳。直接拿JDK 11以上跑老工程,大概率会遇到JCE加密策略或者xml解析相关的报错,倒不是说完全不能修,但首次跑通没必要在这上面耗。

导入IDE时注意保持工程目录结构,别动lib目录下的jar包。农行提供的加密jar包是放在lib里引用的,如果你用Maven把它重新整理,很容易出现依赖冲突,尤其是当本工程里还有公司统一二方库的时候。稳妥做法是先按普通Web工程导入,跑通了再考虑迁移。

3.2 核心配置项:每个参数对应什么

demo里一般有一个MerchantConfig类的配置文件,可能是properties也可能是硬编码。上线前所有跟商户相关的参数都要换掉。下面这份配置结构是这类demo最常见的款式,字段名各家版本略有差别,但逻辑一致:

# 商户号,农行分配,测试环境和正式环境不一样 merchantId=103100000000123 # 商户证书路径,pfx格式 merchantCertPath=/opt/cert/merchant.pfx # pfx证书密码,申请时设置的 merchantCertPwd=ChangeMe # 农行公钥路径,cer格式 bankCertPath=/opt/cert/bank.cer # 支付网关地址,测试和正式是两套 payGatewayUrl=https://paygateway.test.example.com/pay # 支付完成后跳转页面地址 returnUrl=https://www.example.com/pay/return # 异步通知地址,农行后台回调 notifyUrl=https://www.example.com/pay/notify

这段配置的要点是:商户号、证书路径、网关地址三者是一套对应关系。测试环境必须用农行给的测试商户号和测试证书,正式证书在测试环境反而可能报证书无效。returnUrl是用户支付完看到的页面,notifyUrl是农行服务端异步通知商户系统的地址,通知地址必须是公网可达的HTTPS地址,而且不能带端口号中的奇怪参数。

3.3 发起支付:构造表单的代码逻辑

农行web端网银支付的发起方式很传统:商户服务器构造一段自动提交的HTML表单,让浏览器POST到农行支付网关。demo里一般在PaymentServlet里做这件事。核心代码如下:

// 支付初始化:构造签名原串并输出自动提交表单 protected void doGet(HttpServletRequest req, HttpServletResponse resp) { // 1. 从配置读取商户号与回调地址 String merchantId = MerchantConfig.get("merchantId"); String orderId = req.getParameter("orderId"); String amount = req.getParameter("amount"); // 单位:分 String returnUrl = MerchantConfig.get("returnUrl"); String notifyUrl = MerchantConfig.get("notifyUrl"); String currency = "10"; // 人民币 // 2. 按接口文档约定的顺序拼接签名原串 // 字段顺序以农行接口文档为准,注释里标明各字段含义 String plain = merchantId + "&" + orderId + "&" + amount + "&" + currency + "&" + returnUrl + "&" + notifyUrl; // 3. 使用商户私钥签名并做Base64编码 String sign = SignUtil.sign(plain, MerchantConfig.get("merchantCertPath"), MerchantConfig.get("merchantCertPwd")); // 4. 组装自动提交表单,form action指向农行支付网关 String formHtml = buildAutoSubmitForm(merchantId, orderId, amount, currency, returnUrl, notifyUrl, sign); resp.setContentType("text/html;charset=GBK"); resp.getWriter().write(formHtml); }

这段代码里最重要的是第2步的拼接顺序。接口文档里会写清楚先拼哪个字段后拼哪个字段,这个顺序必须原样保留,不能因为觉得“这样拼更合理”就调整。金额字段注意单位是分,而且整个拼接过程完全用字符串,不要用double或者float去乘100,浮点误差在支付场景里是不能接受的。

发起支付这一环还有一个容易被忽略的点:form表单的method是POST,目标地址是农行网关,而不是returnUrl。returnUrl只是农行支付完成之后跳转回商户网站的地址,它不承担接收报文的任务。

3.4 接收回调:验签与幂等处理

农行的支付结果通知走的是后台异步通知,通知地址就是notifyUrl。这个方法必须能接收POST请求,验签通过后更新订单,并向农行返回固定的成功标识。demo里的NotifyServlet骨架基本都是这样:

// 支付结果回调:验签 -> 更新订单 -> 返回成功标记 protected void doPost(HttpServletRequest req, HttpServletResponse resp) { // 1. 按文档约定收集回调字段,拼接验签原串 Map<String, String> params = new HashMap<>(); String signValue = req.getParameter("signValue"); String signPlain = buildPlainText(req); // 与文档签名字段顺序保持一致 // 2. 用农行公钥验签,防止伪造通知 boolean passed = SignUtil.verify(signPlain, signValue, MerchantConfig.get("bankCertPath")); if (!passed) { resp.getWriter().print("fail"); // 验签失败,不返回成功标识 return; } // 3. 幂等:先查订单状态,已支付直接返回成功 String orderId = params.get("orderId"); if (orderService.isPaid(orderId)) { resp.getWriter().print("SUCCESS"); return; } // 4. 落库更新订单,附带银行流水号 orderService.markPaid(orderId, params.get("bankSerialNo")); resp.getWriter().print("SUCCESS"); }

回调处理有三个硬性要求:验签必须失败就拒绝;返回的响应体只能是农行规定的纯文本成功标识,不能返回JSON、不能返回HTML;在返回SUCCESS之前,业务逻辑必须全部执行完。农行收到这个标识才会认为通知成功,如果它超时没收到或者收到的内容不对,会按策略重发通知。所以这里不能把待发货这种异步操作提前执行,必须在标记支付成功之后,后续再触发。

3.5 测试环境自测清单

在跑通demo之后,别急着让农行那边联调,先在测试环境把链路走完整。下面是每次接这种支付我都要过一遍的清单:

检查项通过标准常见失败点
证书加载demo启动日志无证书错误pfx密码错、证书路径使用相对路径
发起支付浏览器能跳转到农行收银台网关地址配错、签名失败
同步跳转支付完成能回跳returnUrlreturnUrl带特殊字符、编码问题
异步通知本地能收到POST回调内网穿透失效、notifyUrl未配置公网
验签回调验签通过签名原串拼接顺序不一致
幂等重复回调不重复发货未加订单状态判断
编码中文商品名显示正常请求或回调未用GBK

4. 农行web端支付避坑实录:证书、编码和回调重复通知的五个现场

这一章把我实际见过、自己也踩过的五类问题列出来,每一条都按现场现象、根因、解决方法来写。如果你正在联调农行web端网银支付,这五条大概率能撞上至少一条。

4.1 回调内容全是乱码

现象:农行异步通知收到的中文参数显示为乱码,订单里的商品名完全不可读。原因:农行网关按GBK编码发送通知内容,而demo工程默认用UTF-8解码。解决:在接收回调的Servlet入口处调用req.setCharacterEncoding("GBK"),读取参数后再做业务处理。如果用了框架,还需要检查框架的编码过滤器,防止它先按UTF-8把参数解析掉。

4.2 验签永远失败

现象:发起支付时农行返回验签失败,日志里只给出一个模糊的错误码。原因:最常见的是签名原串拼接顺序和接口文档不一致,其次是拼接的字符串编码不是GBK,或者拼接时带了不可见字符。解决:把demo里拼接签名原串的代码和接口文档的示例报文逐字符核对,尤其在订单号后面检查有没有多余空格。我自己调试时习惯把拼接后的字符串打印出来,用十六进制查看尾部是否藏着换行符。

4.3 支付成功但订单没入账

现象:用户在农行收银台支付成功,商户后台订单状态还是待支付。原因:异步通知处理代码里业务异常,或者响应内容不是农行规定的成功标识。很多人在回调Servlet里返回了JSON格式的{"code":200},农行不认这个结果,会判定通知失败并重发。解决:回调方法的返回内容仅允许农行文档指定的纯文本成功标识,比如SUCCESS。如果业务处理抛异常,也要捕获后在finally位置输出成功标识,确保订单状态不会被漏更。

4.4 重复回调导致重复发货

现象:同一笔订单收到多次通知,后台记录里出现两条发货记录。原因:农行在没收到成功确认或网络超时的情况下会重发通知,如果没有做幂等处理,同一个orderId会被处理两次。解决:在更新订单的SQL里加状态条件,比如UPDATE orders SET status='PAID' WHERE order_id=? AND status='UNPAID',更新行数为0说明之前已经处理过。demo里那一段isPaid判断是必须保留的,不能因为“现在农行重发没那么频繁”就删掉。

4.5 新JDK环境跑demo报算法异常

现象:工程在JDK 8下正常,换到JDK 11报InvalidKeyException或NoSuchAlgorithmException。原因:老demo里可能硬编码了旧的算法名,或者JCE的默认策略在新版本里发生了变化。解决:先看异常堆栈里的算法名,和接口文档对比,改成文档支持的标准写法。如果只是跑demo验证逻辑,最省事的方式是换回JDK 8配Tomcat 8,让环境和demo的年代匹配,业务侧再考虑升级问题。

提示:第4.2和4.4这两条,既影响接口联调也影响生产安全,建议代码Review时作为必查项。支付回调的幂等不是可选项。

5. 把demo改造成能上线的支付模块:三个值得先做的重构动作

demo跑通只是开始,直接拿demo打生产包会留下不少隐患。我的做法是抽出下面这三个重构动作,每接一次银行支付都先做完再谈联调。

第一个动作是把支付逻辑从Servlet里挪到一个独立的Service类。Servlet是Web层,不该写签名、拼报文的逻辑。我会定义这样一个接口,让controller只做参数接收和视图返回:

public interface PayService { // 创建支付表单,返回自动提交HTML String createPayForm(PayOrder order); // 处理异步通知,返回农行要求的应答文本 String handleNotify(PayNotify notify); }

实现类里放证书加载、签名原串拼接、验签和幂等判断。这样写的好处是单元测试能直接调Service验证签名逻辑,不必启动Tomcat。第二个动作是把证书密码和网关地址从代码里挪出去,放到配置中心或者环境变量里。农行证书密码放在代码仓库里是安全隐患,而且换证书时还得重新发版。我一般用环境变量注入密码,配置项只保留路径。

第三个动作是加一个主动查询订单状态的兜底定时任务。支付回调可能在极端情况下丢失,农行提供的订单查询接口就是为这个准备的。定时任务每天扫描超过N小时仍处于待支付状态的本地订单,调用查询接口确认银行侧状态,再做补单处理。这不是多余工作量,银行接口偶尔就是会让人体验一下“玄学”。

从那以后我每次接银行支付,都会强制自己先走完这三个动作再进入联调窗口,回调幂等和主动查单这两件事绝不在上线前夜补。一次支付回调丢失引发的工单,远比写这几行代码耗时。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询