简介:农行Web端网银支付Java接口升级包及示例工程,面向需要集成农业银行B2C网银支付的Java后端开发者和企业项目团队。压缩包共147个文件,大小5.1MB,包含class接口封装类、JSP演示页面、HTML说明页、JAR依赖库、证书及密钥库文件等,覆盖从商户参数配置、签名验签、交易请求提交到银行异步回调的完整调用链路。已有2073人次浏览学习。这份资料的价值主要体现在几个方面:一是可快速理解农行网银支付的接口字段与报文规范,尤其是签名与验签逻辑;二是示例代码提供了清晰可运行的调用流程和前后端交互页面,便于基于现有JSP工程改造成生产环境;三是包内附带cer证书、truststore及多种配置样例,方便排查证书加载、参数编码、回调验签等高频问题。无论是初次接入农行支付,还是已有项目需要升级接口能力,这套资源都能提供直接的参考代码与配置模板。
1. 农行web端网银支付Java接口:为什么开发包里躺着十几年前的demo
作为Java工程师,在一个需要对接B2C网银支付的web项目里,最常被转交的任务就是“这有份农行的java接口文件和demo,你研究一下”。我一开始以为打开demo就能看到整洁的Spring Boot工程,结果看到的是一堆JSP、XML和本地jar包,连pom.xml都没有。农行web端网银支付java接口,说到底就是商户系统与农行支付网关之间的报文交换:商户用证书对订单签名,把请求发到农行收银台,再接收网关的同步跳转和异步通知。这套东西不复杂,但老接口的老规矩多,签名顺序、编码、证书密码全是坑。这篇文章就是写给正在接这个接口、又不想反复翻车的Java后端,读完能直接照着把demo跑通并改造成自己的业务。
2. 把农行接口文件和demo工程跑起来:从解压到本地出支付页
2.1 接口文件和demo里到底有什么
农行给的开发包通常是一个压缩包,里面混合着中文目录、老版本Eclipse工程、war包、接口说明文档和证书样例。第一次打开时不要慌,跟“金融级接口”这几个字比,实际内容反而更像一个古董展示区:文档里是几组报文格式定义,demo里是几个JSP页面和一个用于签名的工具类,剩下的就是证书文件和一堆老版本的依赖jar包。
这里要分清你拿到的是哪一类接口,农行web端网银支付做过多次改版,有的走“后台密文报文+前台跳转”,有的走“页面表单直投”。最常用的B2C网银支付,特征是文档里同时出现“支付请求”“回调通知”“订单查询”三组报文,并且给了商户私钥和农行公钥两种证书。拿到了这样的东西,基本就可以确定是标准网银跳转模式。
我一般会先把开发包里的接口说明文档找出来,按文档里的报文目录建一个小清单:支付请求需要哪些字段、回调验签用哪把钥匙、查询接口的签名串怎么拼。这一步比急着改代码重要得多,因为农行demo里的代码不一定和最新文档完全一致,文档里没写的字段,代码里写了也没用。等清单理出来,再看demo的目录结构心里就有底了。
2.2 把demo导入IDE的步骤
老demo不是Maven工程,强行用IDEA的Maven或Gradle方式打开,会把lib目录下的本地jar丢失。我一般会直接按普通Java项目导入,然后把它部署到Tomcat里跑。下面是能稳定走通的最小步骤,适用于大多数农行web端网银支付的Java版demo。
- 先确认JDK版本。农行老demo很多是按JDK 1.4或1.6编译的,用太高版本的JDK跑会报
UnsupportedClassVersionError,建议先切到JDK 1.8,兼容性最好。 - 在IDEA或Eclipse里选择普通Java项目导入,不要选Maven项目。导入后检查lib目录是否被识别,没识别到的jar就手动Add as Library。
- 找到Web配置,确认是一个Servlet/JSP工程,然后配置Tomcat,比如Tomcat 8.5配JDK 1.8,这个组合跑农行老demo问题最少。
- 找到demo里的配置文件,通常是
merchant.properties或MerchantConfig.xml,把里面的商户号、证书路径、证书密码改成自己的。 - 启动Tomcat,访问demo自带的支付测试页。如果能看到一个可以填写订单金额、订单号的页面,说明工程已经跑通了。
配置文件内容示意如下:
# 农行网银支付demo配置文件(示例) merchant.id=103123456000001 merchant.pfx.path=/WEB-INF/conf/merchant.pfx merchant.pfx.password=yourCertPwd merchant.cer.path=/WEB-INF/conf/abc.cer gateway.pay.url=https://payment.abchina.com.cn.example/ebus/Pay gateway.query.url=https://payment.abchina.com.cn.example/ebus/Query这段配置里,merchant.id是商户签约后拿到的虚拟商户号,不是合同编号,填错后面会一直报“商户不存在”。merchant.pfx.path是商户私钥文件的位置,demo里通常已经放了一个测试用pfx,你上线前要替换成生产证书。merchant.pfx.password是私钥口令,农行开户资料里会给,注意这个密码可能包含特殊字符,从PDF复制出来时经常带不可见空格,这是后面证书加载失败的常见原因。merchant.cer.path是农行公钥证书路径,只有同时存在这个文件,回调验签才有依据。网关地址在配置里是坑,这里的地址只是示例,正式对接时以农行商户服务资料为准,不要拿网上搜到的地址硬填。
2.3 为什么需要证书和一台能访问外网的web容器
农行web端网银支付接口的鉴权方式不是用户名密码,而是证书签名。商户发起支付请求时,用merchant.pfx里的私钥对订单关键字段做签名,农行收到后用商户公钥验签;农行回调商户系统时,农行用自己的私钥签名,商户系统用abc.cer验签。所以证书密码错、证书路径不对、cer文件缺失,都会导致支付请求在网关侧直接失败,而且失败报文往往只有“格式错误”这种模棱两可的描述。
本地调试时还有一个现实问题:农行回调要访问你的web项目,农行网关在商户端返回回调通知时,必须要有一个可访问的HTTP地址。如果你只是在本机启动Tomcat,农行回调会失败。常见做法是在开发机用一台有公网IP的测试服务器部署demo,或者在本地用内网映射工具把8080端口暴露出去,这样回调地址才能被网关访问到。我一般会先在测试服务器上把demo跑通,再回本地开发改造,避免业务代码还没写就被网络环境卡住半天。
3. 支付请求与RSA签名:签名串顺序、证书参数和网关跳转
3.1 签名原理:别自己发明签名串顺序
农行web端网银支付的支付请求,本质是把一批订单参数加上一个签名,打包成HTTP表单提交到网关。签名的作用有两个:一是证明请求来自该商户,二是防止订单金额、订单号在传输中被篡改。整个过程不依赖登录态,只依赖商户私钥和农行公钥这对证书关系。
签名串的顺序是整件事里最不能自由发挥的地方。我遇到过很多回“按自己觉得合理的顺序拼签名串”,结果网关返回“验签失败”。农行接口文档里通常会给一个明确的字符串拼接顺序,比如订单号、订单金额、订单日期、订单时间、支付方式、币种、商户号。你去看demo里的签名工具类,最终生成的签名原文也和这个顺序对应。所以第一步应该是打开demo里负责签名的方法,把里面的拼接逻辑抄下来,而不是自己重新设计。
一个常见的支付请求签名示例是这样的:
// 构造支付请求参数(示意代码,字段顺序以农行文档为准) Map<String, String> params = new LinkedHashMap<>(); params.put("MerchantID", merchantId); // 商户号 params.put("PayType", "A"); // 借记卡支付 params.put("OrderNo", orderNo); // 商户订单号 params.put("OrderAmount", amount); // 金额,单位与位数看文档 params.put("OrderDate", dateStr); // yyyyMMdd params.put("OrderTime", orderTime); // HHmmss params.put("CurType", "01"); // 人民币 params.put("Priv1", priv1); // 商户保留域 // 按文档顺序拼签名原文 String source = params.get("OrderNo") + params.get("OrderAmount") + params.get("OrderDate") + params.get("OrderTime") + params.get("PayType") + params.get("CurType") + params.get("MerchantID"); // RSAUtil 一般由 demo 自带,不要自己重复造轮子 String sign = RSAUtil.sign(source, merchantPrivateKey); params.put("Sign", sign);这里有个容易踩的坑:LinkedHashMap保证插入顺序,但最终发送给网管的表单字段顺序并不等于签名顺序。网关验签时只认签名原文,不认报文里的字段物理顺序。所以就算你把参数打乱放到表单里,只要签名串是按文档顺序拼的就没问题。RSAUtil.sign内部一般会先对原文做摘要,再做私钥签名,最后输出Base64字符串。第三方开发者在没有农行工具类时,容易在摘要算法上抓瞎,有的版本用MD5,有的用SHA-1,有的用SHA-256,同一套demo的不同接口用的算法都可能不同。所以不要另写一套签名工具,直接用农行demo里那个类最省心。
3.2 支付请求参数表:必填项与容易填错的字段
农行支付请求的参数不多,但每个字段都需要认真核对。下面是我整理的一个参考对照,具体以你手里的接口文档为准。
| 参数名 | 含义 | 常见填法 | 容易出错的点 |
|---|---|---|---|
MerchantID | 商户号 | 签约后生成的虚拟商户号 | 填成合同号或柜台号 |
OrderNo | 商户订单号 | 业务系统订单号 | 重复提交,网关拒收 |
OrderAmount | 订单金额 | 字符串,保留两位小数 | 分和元的单位换算搞反 |
OrderDate | 订单日期 | yyyyMMdd | 格式多了横杠 |
OrderTime | 订单时间 | HHmmss | 缺少前导零 |
PayType | 支付类型 | 借记卡/贷记卡/混合支付 | 网银支付与快捷支付混淆 |
CurType | 币种 | 01人民币 | 用CNY等错误写法 |
Priv1 | 商户保留域 | 业务ID、用户ID | 存放中文导致编码问题 |
金额单位是这个接口里最值得写进代码注释的坑。农行老接口里,订单金额不少地方以“元”为单位,保留两位小数,但也有一些扩展接口要求以“分”为单位。我接过的版本里,直接由demo的金额格式化方法决定,你只要跟着demo走就没事。如果自己写转换,建议在代码里硬编码一个parseAmount方法,把“元转分”或“分转元”的规则写在注释里,方便后面的人少踩一次坑。
订单号也需要注意,农行网关对订单号长度和字符集有限制,一般只允许字母和数字,以及少量符号。如果业务订单号里有横杠、下划线倒是没问题,但若要放中文或空格,网关大概率会返回格式错误。我一般会在订单号上套一层白名单过滤,宁可多写一个方法,也不让脏字符进入报文。
3.3 发起支付跳转:用隐藏表单,不要用Ajax
支付请求是页面跳转型交互,商户后台构造完参数后,要把用户浏览器引导到农行收银台。很多人第一次做会试图用Ajax或HttpClient后台重定向,这是错的。农行网关要求浏览器整页跳转,后台发请求只能拿到网关的HTML响应,不会帮用户完成后续支付。标准做法是在JSP或Servlet中输出一个自动提交的HTML表单。
// 从请求参数中拼出表单HTML并输出到页面 // 这里示意把参数按表单字段回显,注意字段名大小写 StringBuilder html = new StringBuilder(); html.append("<form name=\"payForm\" action=\"").append(gatewayUrl) .append("\" method=\"post\" style=\"display:none\">"); for (Map.Entry<String, String> entry : params.entrySet()) { html.append("<input name=\"").append(entry.getKey()) .append("\" value=\"").append(htmlEscape(entry.getValue())) .append("\"/>"); } html.append("</form>"); html.append("<script>document.payForm.submit();</script>"); // 在Servlet里输出html response.setContentType("text/html;charset=GBK"); response.getWriter().write(html.toString());这个示例要解决两个关键点:第一,表单字段名大小写必须与农行网关严格一致,比如MerchantID中间的I是大写,写错成小写Merchantid网关不认。第二,输出页面的字符集要跟着农行接口要求走,老接口经常要求GBK,如果这里用UTF-8输出,中文订单描述会变成乱码。我一般把页面字符集也写进配置,不要和项目其他页面共用一套默认值。
还有一点,支付请求里的金额和订单号在生成HTML前最好再做一次数据库快照写入。因为用户可能重复提交、刷新页面,若没有在进入网关前把订单锁定,后面回调到了都不知道是哪一笔。我习惯在生成表单前把订单状态置为“支付中”,并记录请求的完整报文,这样排查问题时有日志可查。
4. 支付结果回调与主动查询:异步通知别只信一次,对账要双轨
4.1 回调通知的可靠性问题
农行网关在用户支付完成后,会向商户系统发起异步通知,告诉你这笔订单支付成功。听起来简单,但实际工程里不能只依赖这记通知。农行回调可能因为商户系统短暂不可用、网络超时、返回报文体不合法而连续重发;也可能由于用户关掉页面、网关侧异常而一直没发出来。所以回调处理的代码必须做成幂等的:收到一次和收到十次,结果都要一样。
我一般会在回调接口里做三件事:先验签,然后查本地订单状态,最后只在“待支付”状态下更新为“已支付”。如果订单已经支付,直接返回成功标记,不做重复更新。这样能避免重复入账、重复发物流、重复发短信。回调接口里也不要写太多业务逻辑,耗时超过农行等待时间,网关就会判定失败并重发。更稳妥的做法是回调里只更新订单状态和落一条通知日志,把后续的ERP通知、库存扣减放到队列里异步消费。
4.2 主动查询接口:demo里的orderQuery
主动查询是支付结果兜底的关键接口。农行web端网银支付demo里一般会有一个查询订单的状态页或Servlet,用来向网关注销订单支付情况。主动查询的签名规则和支付请求类似,只是需要查询的字段更少,通常只需要订单号、订单日期和商户号。
// 主动查询农行订单状态(示意代码) String source = orderNo + orderDate + merchantId; String sign = RSAUtil.sign(source, merchantPrivateKey); Map<String, String> queryParams = new LinkedHashMap<>(); queryParams.put("MerchantID", merchantId); queryParams.put("OrderNo", orderNo); queryParams.put("OrderDate", orderDate); queryParams.put("Sign", sign); // 使用 HttpClient 发起 POST 请求到 gateway.query.url HttpPost post = new HttpPost(queryUrl); List<NameValuePair> pairs = new ArrayList<>(); for (Map.Entry<String, String> e : queryParams.entrySet()) { pairs.add(new BasicNameValuePair(e.getKey(), e.getValue())); } post.setEntity(new UrlEncodedFormEntity(pairs, "GBK")); try (CloseableHttpResponse resp = httpClient.execute(post)) { String result = EntityUtils.toString(resp.getEntity(), "GBK"); // 解析 result,找到状态字段,判断是否支付成功 }这里用到了HttpClient,因为主动查询不需要用户浏览器参与,后台直连网关更合适。注意UrlEncodedFormEntity的字符集同样要跟农行接口文档保持一致,很多线上查询乱码或验签失败,都是因为这一行用了默认的ISO-8859-1。查询返回的报文里,支付状态字段的值可能是数字也可能是字母,常见的是00表示成功,但不同版本不一样。不要硬编码,从demo的解析代码里找到这个字段的定义。
查询接口的使用场景一般是三块:用户支付完关掉了浏览器、支付结果回调超时没有到达、以及深夜对账时逐笔核对。我把主动查询封装成一个queryPaymentStatus方法,入参只有一个内部订单号,方法内部先查本地订单的订单号和订单日期,再组装查询请求。这样业务代码不会散落着农行参数。
4.3 回调与查询不一致时以哪个为准
实战中会遇到很尴尬的情况:主动查询返回“支付成功”,但异步回调还没来,或者来了验签不过。这时候很多人的第一反应是“以查询为准”,但这不一定对。农行的回调是支付流程的最终结果事件,查询接口返回的更多是当前状态快照。两者都可能是对的,只是到达商户系统的时间不同。
我使用的规矩是:回调验签通过,优先以回调更新订单状态;回调缺失,则用查询结果兜底。兜底时不要直接改订单状态,而是把查询结果记到一张“支付结果核对表”里,再由一个定时任务去匹配本地订单。如果本地订单仍是待支付,就把它改成已支付,并补记备注“经查询接口确认”。如果本地订单已经标记为已支付,就什么都不用做。这样既不会重复入账,也不至于因为一条没收到回调的订单卡住业务。
5. 农行网银支付Java接口的避坑与常见问题:5个翻车现场
5.1 现象:本地能跑通,线上报“证书验证失败”
本地demo一切正常,换到生产服务器后,第一次支付请求就返回“证书验证失败”。我刚开始以为是环境少了证书文件,反复检查路径都没问题,后来发现是证书密码在生产配置里多了个不可见字符。因为生产配置项是从运维平台的文本框里复制过来的,密码末尾带回车符。把密码用程序打印成字节数组,肉眼就能看到多了\r。
解决方法是不要直接复制配置,在properties文件里把密码写成一个带引号的字符串,或者从环境变量读取后再手动trim()。另外,农行有测试证书和生产证书两套,检查一下是不是把测试pfx传到了生产服务器。测试证书和生成的商户号不匹配时,网关也会报证书验证失败。
5.2 现象:回调验签一直false
回调接口什么都收到了,但RSAUtil.verify返回false。这个坑很隐蔽,问题往往不在签名算法,而在验签之前对签名原文或签名字段动了手脚。比如有人为了排查,把sign字段里的加号、斜杠做了URL解码或HTML转义,还有人把原文里的订单号.trim()了一下,有用没用的空格都影响了签名结果。
解决方法是验签前不要对原始参数做任何trim、replace、decode。直接在回调接口第一行把收到的所有参数原样存日志,再用demo里自带的验签方法验。如果demo验签能过,说明你的处理逻辑有问题;如果demo验签也不过,再对比签名原文是否和文档一致。我习惯把签名原文和签名串都打成十六进制日志,对比时方便多了。
5.3 现象:订单中文描述乱码
支付请求里有一个订单描述字段,填了中文,结果到农行收银台页面显示乱码,或者农行立即返回报文编码错误。原因基本都能锁定在字符集上。农行老接口对中文描述使用GBK编码,而Java web项目现在大多默认UTF-8,表单跳转时没有把字段值按GBK编码发送。
解决方法是给生成支付的form标签加上accept-charset="GBK",同时设置response.setContentType("text/html;charset=GBK")。如果使用HttpClient方式提交,所有UrlEncodedFormEntity也统一用"GBK"。改完这个之后,中文描述乱码基本消失。还有一个伴随坑,数据库里存的订单描述本身是UTF-8,发送前需要按GBK重新编码,这时候new String(desc.getBytes("UTF-8"), "GBK")这种写法会让人头疼,最省事的办法是让代码统一按GBK读取配置和请求参数。
5.4 现象:调用查询/退款接口一直报“商户不存在”
支付请求能正常跳转,但查询接口或者退款接口返回“商户不存在”。这个现象很迷惑,因为如果是商户号错,支付请求也应该失败。后来仔细看文档才发现,农行的支付网关和查询/退款网关可能使用不同的商户号格式,有的场景要求商户号后面补零,有的场景要求不带地区码。
解决方法是把支付请求里的商户号和查询/退款请求里的商户号分开配置,不要复用同一个字段。另外,农行部分接口要求请求IP在商户服务后台登记白名单,测试服务器的IP没加白名单,也会报类似“商户不存在”或权限错误。我一般把商户号和绑定IP写在同一个上线检查清单里,部署前逐项核对。
5.5 现象:demo的Tomcat一启动就报各种Servlet API警告
老demo在Tomcat 9、10上启动时,控制台刷一堆ClassNotFoundException或NoClassDefFoundError,这是因为新Tomcat把javax.servlet迁移到了jakarta.servlet。农行老版demo用的是旧API,硬跑在新容器上,连JSP都编译不过。
解决方法是先不要追求新容器,用Tomcat 8.5或9.0配JDK 8跑demo即可。如果项目本身已经迁移到Spring Boot 3.x,无法切回旧容器,那就不要直接塞老demo的JSP,而是把demo中的签名工具类和报文工具类抽取成普通Java类,放到Spring Boot工程里,Servlet部分自己重写。注意这时候javax.servlet和jakarta.servlet的类要拆干净,农行工具类里如果有直接依赖HttpServletRequest的,需要简单适配一下。这样做虽然要花一点时间,但比在微服务环境里强行部署一个古董war包干净得多。
6. 从demo到web项目:最小改造、验证清单和上线前最后一小时
6.1 把demo逻辑收进一个PayService
demo里的支付逻辑散在JSP和Servlet里,直接拿到web项目里会到处都是农行参数。我一般会把demo中的配置读取、签名、请求发送、回调验签四件事封装成一个PayService。核心方法只有两个:buildPayForm(order)负责生成支付表单,handleNotify(request)负责验签和更新订单。这样改造后,控制器里不会出现任何农行签名逻辑,后续换其他支付通道也更容易。
6.2 上线验证清单与自测命令
上线前一个小时,我建议按下面这个清单逐项过一遍。
- 在测试环境用一分钱商品发起支付,确认能从农行收银台完成支付,并收到回调。
- 支付完成后,立刻调用一次主动查询接口,确认查询结果与回调一致。
- 把回调接口临时改成返回错误,观察农行是否重发,确认不出重复入账。
- 用GBK编码传一个中文订单描述,确认收银台显示不乱码。
- 停掉应用,模拟回调丢失,重启后主动查询能否兜底更新订单。
最后把这个接口的流程图画在代码注释里,方便下一个维护的人。我第一次接这个接口时,把签名串顺序按自己的习惯排,死在“报文格式错”上两天,后来发现demo就是最权威的参照实现,自己造轮子只会更坑。做老接口对接,规矩比创意重要,老老实实跟着demo走,再按业务需求做减法,就能少踩很多坑。希望帮到你。
本文还有配套的精品资源,点击获取