简介面向需要在Java环境中集成农业银行网银支付功能的开发者提供完整接口文档与可运行Demo。压缩包共147个文件、约5.1MB包含class字节码、jsp动态页面、html说明页面、jar依赖库、properties配置及cer证书、truststore信任库等类型覆盖交易请求、签名验证、响应处理、回调通知等完整流程。资源内含升级版Java接口包涉及商户参数配置、签名服务、数据验签、证书管理等核心类并配有供参考的页面与配置样例可帮助开发者快速理解农行网银支付对接原理在此基础上进行二次开发。已有2076人学习下载适合具备一定Java基础、正在对接农行支付渠道的技术人员参考。1. 农行web端网银支付Java接口这份demo到底解决什么问题接到一个自营商城的支付需求客户甩过来一份“农行web端网银支付Java接口文件及demo”压缩包里面是接口文档、示例代码和一张测试证书。初次对接的人很容易把这当成普通HTTP接口拼好参数、发个请求、等回调完事。真正动手才发现网银支付的坑和微信、支付宝完全不同——GBK编码、RSA签名、证书文件、遮挡页跳转、回调验签每一步都可能让联调卡上几天。这份资源的价值在于它把农行B2C支付从下单到收到异步通知的完整链路用Java demo串了一遍适合两类人一是要给自营商城、订单系统接入网银支付的后端开发二是需要给客户演示“支付功能已经调通”的项目实施人员。它不解决业务问题但能帮你把银行侧的对接逻辑一次性跑通。2. 农行网银支付接口机制先看懂报文、签名与回调再写代码网银支付和扫码支付最大的区别在于它不是一次API调用就完成的。用户要跳转到银行页面、登录、输密码、再跳回商户页面中间涉及商户系统、银行前置、银行核心三方的多次交互。demo里最容易被忽略的是它把这个完整交互链拆成了几个独立环节每个环节都有独立的报文格式和验签要求。2.1 接口文档里真正要读的五张表而不是通读全文我拿到接口文档的第一件事不是从头翻而是先找五张表交易类型对照表、请求地址对照表、报文要素表、签名与验签说明、回调通知字段表。这五张表定了整个对接的边界。交易类型对照表解决的是“我该用哪个接口”。农行web端网银支付通常涉及支付下单、交易查询、退款这几个核心动作。demo里一般只演示了支付下单因为退款和查询往往要走单独的商户服务平台或者需要额外的权限申请。你先确认自己拿到的是B2C接口文档还是B2B接口文档两者报文头、签名方式、甚至字符集都可能不同。请求地址对照表解决的是“往哪发”。注意区分测试地址和正式地址很多人联调阶段一切正常切生产后请求地址没换直接报“商户不存在”。报文要素表是最关键的一张表它列出每个字段的名称、类型、长度、是否必填。网银支付里最常见的坑是字段名大小写——文档写的是MerchantID代码里写merchantId银行端解析直接失败。签名与验签说明决定了你能不能跑通第一步。农行用的通常是RSA签名私钥在商户手里公钥在银行手里。demo里的签名工具类基本可以直接用但你要搞清楚它是对哪些字段、以什么顺序拼接后再做摘要的。回调通知字段表解决的是“支付成功后怎么确认”。银行会往你配置的回调地址发一个通知通知里带着交易状态和签名。你必须在回调里做两件事验签确认通知来自银行以及校验订单号和金额防止伪造回调。提示拿到接口文档先不要写代码。先把这五张表的内容和demo代码里的常量、配置对应上能省掉后面一大部分联调时间。2.2 与微信、支付宝支付的三个关键差异做过微信支付的人上手农行网银支付最容易带着惯性翻车。三个差异必须提前建立认知。第一编码方式不同。微信支付宝全是UTF-8农行网银支付很多接口文档明确要求GBK。如果你的服务默认UTF-8拼接报文、签名、提交表单时不做编码转换中文商户名称会乱码更严重的是乱码会导致签名不一致。demo里一般会在报文拼接处显式指定GBK你搬代码的时候注意别把encoding参数顺手改成UTF-8。第二签名粒度不同。微信支付宝支付一般是对整个请求体做签名农行网银支付是“先拼明文再对明文做摘要摘要参与签名”而且不同接口签名字段不一样。有的接口签的是有值字段的拼接串有的签的是固定顺序的全字段串。这个细节直接在签名工具类里写死你要是改需求加了自定义字段签名必须同步调整。第三交互方式不同。微信支付宝是纯后台API交互支付页在你自己的H5里。农行网银支付是前台跳转加后台通知用户会被带到银行页面完成付款银行再通过returnUrl同步跳转和notifyUrl异步通知两个地址把结果送回商户系统。同步跳转只是告诉用户“交易已受理”真正的成功判断必须依赖异步通知。demo里如果只处理了同步跳转而没写异步通知的接收逻辑那这个demo是不完整的你需要自己补。3. 工程落地把农行支付demo跑起来需要准备哪些东西demo不是解压就能跑的。它依赖特定的jar包、证书文件和配置文件任何一个环节缺失都会在运行时才爆出来。这一章把工程准备阶段的细节拆开讲清楚。3.1 工程结构与jar包来源先理清依赖再谈运行典型的农行支付demo解压后是这样一个结构abc-pay-demo/ ├── src/main/java/ │ ├── com/demo/pay/ │ │ ├── util/RSAUtils.java │ │ ├── util/HttpUtils.java │ │ ├── config/PayConfig.java │ │ └── servlet/PayServlet.java ├── config/ │ ├── merchant.cer │ ├── merchant.pfx │ └── pay.properties ├── WebContent/ │ ├── WEB-INF/lib/ │ │ ├── abc-pay-core.jar │ │ └── commons-httpclient.jar │ └── index.jsp └── README.txt我不建议直接复制WEB-INF/lib里的jar包到你的Spring Boot项目。老demo多为Servlet工程依赖了commons-httpclient这类老库和Spring Boot内置的RestTemplate、HttpClient可能冲突。常见做法是只保留RSAUtils和核心签名工具类把HTTP发送逻辑替换成你项目里已有的HTTP组件。RSAUtils类里一般长这样public class RSAUtils { // 读取商户私钥base64解码后用于签名 public static String sign(String content, String privateKeyPath, String password) { try { KeyStore ks KeyStore.getInstance(PKCS12); FileInputStream fis new FileInputStream(privateKeyPath); ks.load(fis, password.toCharArray()); PrivateKey privateKey (PrivateKey) ks.getKey(merchant, password.toCharArray()); Signature signature Signature.getInstance(SHA1withRSA); signature.initSign(privateKey); signature.update(content.getBytes(GBK)); byte[] signed signature.sign(); return Base64.getEncoder().encodeToString(signed); } catch (Exception e) { throw new RuntimeException(商户签名失败, e); } } }逻辑说明KeyStore加载的是PKCS12格式的商户证书文件.pfx或.p12别名通常是merchant。签名算法是SHA1withRSA这是农行网银支付最常用的组合如果银行端升级成了SHA256withRSA这里要换成对应算法。签名内容按GBK编码取字节这个细节不能改。参数说明privateKeyPath是证书文件的绝对或相对路径password是证书密码。这两个值往往在配置文件和代码里各出现一次改配置忘了改代码或者反过来都会导致签名时拿不到私钥。3.2 商户证书与配置文件里的关键参数配置文件是整个对接中最好复制、也最容易错的部分。demo的pay.properties一般包含这些项# 商户代码由银行分配 merchant.id103310000000000 # 商户证书密码开户时随证书一起下发 merchant.cert.password123456 # 商户证书别名默认merchant不建议改动 merchant.cert.aliasmerchant # 银行支付网关地址测试和正式环境不同 pay.gateway.urlhttps://bank.test.example.com/payment # 同步跳转地址支付完成后浏览器跳回 pay.return.urlhttp://localhost:8080/abc-demo/return # 异步通知地址银行后台发送通知 pay.notify.urlhttp://localhost:8080/abc-demo/notify # 字符集网银支付常用GBK pay.charsetGBK这里要单独提醒merchant.id在demo里往往写了一个测试商户号正式上线时一定要替换成自己申请的商户号。很多人联调时一直用demo里的商户号银行端流水都有但商户后台查不到订单就是因为商户号根本不是自己的。同步和异步通知地址要求公网可访问而且异步通知地址不能带localhost。联调阶段没有公网环境常见做法是用内网穿透工具或者把回调地址配到测试服务器的公网IP上。如果你只是本地模拟同步跳转可能看不出来但异步通知一定收不到。注意证书文件不要提交到Git仓库。.pfx等于商户的支付私钥泄露了别人就能拿你的商户号签名下单。demo里那份是测试证书换正式证书时务必检查私钥文件的权限位。3.3 demo里核心类的调用关系一次性理清demo里最核心的类是PayServlet它做的事可以拆成四步从配置类读取商户号和证书路径。根据请求参数订单号、金额、商品名构造支付报文。用私钥对报文签名。把签名后的参数拼接成HTML表单返回给浏览器自动submit到银行网关。protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(GBK); resp.setContentType(text/html; charsetGBK); String orderId req.getParameter(orderId); String amount req.getParameter(amount); // 单位分 String productName req.getParameter(productName); // 商品名称 // 第一步构造报文明文 String plainText MerchantID PayConfig.merchantId OrderID orderId Amount amount ProductName productName ReturnURL PayConfig.returnUrl NotifyURL PayConfig.notifyUrl; // 第二步签名这里调RSAUtils String signedText RSAUtils.sign(plainText, PayConfig.certPath, PayConfig.certPassword); // 第三步拼隐藏表单字段 StringBuilder html new StringBuilder(); html.append(form idpayForm action).append(PayConfig.gatewayUrl) .append( methodpost); html.append(input typehidden nameMerchantID value).append(PayConfig.merchantId).append(); // 其余字段省略注意最后一个字段是签名值 html.append(input typesubmit value去支付); html.append(/form); resp.getWriter().write(html.toString()); }逻辑说明doGet处理的是一次浏览器请求把用户引导到银行网关。这里的amount单位是分银行端一般要求整数如果你传入100.50这种带小数的字符串会被银行直接拒绝。签名用的是拼接出的明文字符串不是逐字段签。它的顺序必须和银行端验签拼接顺序完全一致顺序错了验签失败。参数说明ReturnURL是同步跳转地址NotifyURL是异步通知地址两个都要完整URL不能只写路径。部分银行把这两个地址配置在商户后台而不是放在请求报文里如果demo代码里发过去了但银行端配置为空以报文为准还是以后台配置为准要跟银行确认通常是以报文里的为准。4. 对接农行web端网银支付下单、跳转、回调验签与查单完整链路这一章把支付完整链路按顺序拆开每个环节给代码和参数说明。整个链路可以概括为后台组单签名 → 前台跳转银行 → 银行扣款 → 异步通知商户 → 商户验签落库 → 同步跳转展示结果。4.1 组单与下单请求字段拼接顺序决定成败下单请求的核心不是HTTP调用而是“怎么拼明文”。银行端验签时会对收到的所有业务字段按固定顺序拼接后验签商户端如果拼接顺序不一致哪怕只是换了一个字段的位置验签必然失败。public String buildOrderMessage(PayOrder order) { StringBuilder sb new StringBuilder(); // 字段顺序必须与银行文档保持一致不能增删改 sb.append(MerchantID).append(order.getMerchantId()); sb.append(OrderID).append(order.getOrderId()); sb.append(Amount).append(order.getAmount()); sb.append(ProductName).append(encodeField(order.getProductName())); sb.append(ReturnURL).append(order.getReturnUrl()); sb.append(NotifyURL).append(order.getNotifyUrl()); return sb.toString(); } private String encodeField(String value) { try { return URLEncoder.encode(value, GBK); } catch (UnsupportedEncodingException e) { throw new RuntimeException(GBK编码失败, e); } }逻辑说明buildOrderMessage只负责把业务字段拼成待签名的明文。ProductName是中文拼接前要做GBK的URLEncoder编码这样签名用的明文和发送给银行的报文才一致。如果这里不编码中文商品名在传输过程中自动变成URL编码格式银行的验签明文里是%E5%95%86%E5%93%81你这边还是中文原文验签必然失败。参数说明Amount的单位是分这一点在demo里容易看走眼——文档里的“金额”经常以“分”为单位但如果测试时填100实际支付金额是1元而不是100元。有的银行支持以“元”为单位通过一个Currency字段表示但农行网银支付常见做法是直接传分。拼好明文后调用签名工具得到Signature字段再把所有字段包括签名按表单格式POST到银行网关。注意这里用的是HTML表单自动提交不是后台HTTP调用。form namepayForm methodpost actionhttps://bank.example.com/payment input typehidden nameMerchantID value103310000000000 input typehidden nameOrderID value20250101120000 input typehidden nameAmount value100 input typehidden nameProductName value%E5%95%86%E5%93%81 input typehidden nameSignature valuexxxxxx /form逻辑说明这里必须用POST提交因为农行网银支付网关不接受GET方式的支付请求。Signature的值是base64编码后的RSA签名长度通常几百字节放在URL里会被截断所以必须走POST。提示如果支付页面加载不出来、白屏先看浏览器的网络面板确认表单提交到了哪个地址以及响应是不是银行的错误码页面。是网关地址配错还是证书、商户号的问题这一步就能区分。4.2 同步跳转与异步回调验签逻辑要写两遍同步跳转ReturnURL是用户支付完成后银行把浏览器重定向回商户页面的入口。同步跳转只做展示不能作为订单完成的依据因为用户可以中途关掉浏览器同步跳转根本不会触发但银行后台还是会把异步通知发过来。异步通知NotifyURL是银行后台往商户服务器发起的POST请求通知里有订单号、金额、交易状态、签名等字段。接收异步通知的接口必须放在公网可达的地址上并且返回success给银行否则银行会认为通知失败按策略重发。protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(GBK); // 读取银行通知的全部参数 EnumerationString paramNames req.getParameterNames(); MapString, String params new HashMap(); while (paramNames.hasMoreElements()) { String name paramNames.nextElement(); params.put(name, req.getParameter(name)); } String signature params.get(Signature); // 剔除签名字段其余字段按银行文档顺序拼接验签 String plainText buildNotifyMessage(params); boolean valid RSAUtils.verify(plainText, signature, PayConfig.bankPublicKey); if (!valid) { resp.getWriter().write(验签失败); // 银行会重试 return; } String orderId params.get(OrderID); String amount params.get(Amount); String status params.get(Status); // 00表示成功 // 业务落库逻辑注意幂等 if (00.equals(status)) { boolean updated orderService.markPaid(orderId, amount); if (updated) { resp.getWriter().write(success); } else { resp.getWriter().write(repeat); // 已处理过返回repeat防止重发 } } }逻辑说明验签用的公钥是银行公钥和商户私钥是配对的。buildNotifyMessage方法里要做一件事——删掉Signature字段然后按银行规定的顺序把剩余字段拼接成明文。这个顺序和下单时拼接顺序不同是回调通知单独的字段顺序。有的银行文档把回调通知的字段顺序写得比较隐蔽藏在“通知报文示例”的字段附注里。参数说明Status的取值银行文档里会给出常见的是00表示成功其他值表示失败或处理中。Amount判断要精确到分注意比较时用字符串或者转成整数不能浮点数比较。orderService.markPaid返回false表示这个订单已经处理过返回repeat告诉银行不用再重发——但前提是你的markPaid在并发情况下能保证幂等否则银行重发通知时可能把订单重复标记为已支付。同步跳转的处理则简单得多protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String orderId req.getParameter(OrderID); String status req.getParameter(Status); // 从数据库查真实订单状态不让用户肉眼看到的参数决定页面展示 Order order orderService.getByOrderId(orderId); if (order ! null PAID.equals(order.getStatus())) { resp.sendRedirect(/success.jsp?orderId orderId); } else { resp.sendRedirect(/pending.jsp?orderId orderId); } }逻辑说明同步跳转接口不建议直接使用银行传来的参数去更新数据库。你无法保证同步跳转和异步通知的到达顺序异步通知先到、同步跳转后到的情况经常出现。这里的代码只负责从数据库查真实状态再展示页面如果订单还没被异步通知更新就展示“支付处理中”等异步通知到达后再刷新页面。4.3 交易查询与定时对账防止“钱到了订单没变”的最后一招异步通知机制不是百分百可靠。银行端网络波动、商户回调接口短暂不可用、代码异常导致没有返回success都可能造成订单已经扣款但商户系统不知道。所以生产环境必须加交易查询和定时对账。public String queryOrder(String orderId) { // 查询报文字段比下单少得多 StringBuilder sb new StringBuilder(); sb.append(MerchantID).append(PayConfig.merchantId); sb.append(OrderID).append(orderId); sb.append(QueryType).append(1); // 1表示按订单号查 String plainText sb.toString(); String signature RSAUtils.sign(plainText, PayConfig.certPath, PayConfig.certPassword); MapString, String params new HashMap(); params.put(MerchantID, PayConfig.merchantId); params.put(OrderID, orderId); params.put(QueryType, 1); params.put(Signature, signature); // 用HTTP客户端发送POST请求 String response HttpUtils.post(PayConfig.queryUrl, params, GBK); return response; }逻辑说明交易查询接口是主动确认订单状态的兜底方案。实现上要注意QueryType这个字段在部分银行版本里存在有的版本用OrderID就能查你以手上的接口文档为准。查询接口的URL和支付网关URL通常不同文档里要单独找。定时对账的做法是每分钟或每五分钟扫描一批状态为“待支付”但超过N分钟的订单。逐个调用queryOrder查状态。如果银行返回成功直接把订单更新为已支付并补齐流水记录。这样即使异步通知因为网络问题丢了对账任务也能在短时间内发现并修正。不要只依赖异步通知这是所有支付对接的共识而不是可选项。5. 农行网银支付对接避坑五个高频故障与排查路径换个角度说这一章是全文最有价值的章节。以下五个问题我在实际对接和帮别人看问题时反复遇到过每条按“现象→原因→解决”整理清楚。5.1 现象报错“验签失败”证书、密钥都没改过联调阶段刚跑通一次第二次再试就报验签失败。排查发现订单参数里多了个字段比如增加了ProductNum商品数量。原因签名用的明文必须包含所有发送给银行的业务字段。在demo原始代码里ProductNum不在签名拼接串里你把它加到了表单里但签名还是只对原来的字段做。银行端验签时会把收到的全部字段拼起来验多出来的字段不在签名串里拼出来的明文自然和你的签名不匹配。解决凡是请求报文里新增的业务字段必须同步加进签名明文的拼接串并且位置要和银行端拼接顺序一致。5.2 现象异步回调收到POST但是报文字段全是乱码回调接口里打印日志发现ProductName、MerchantName这些字段的值是乱码。原因回调请求按GBK编码发送而Servlet容器或Spring MVC默认按ISO-8859-1或UTF-8解码。具体来说Tomcat 8及以上默认URI编码是UTF-8POST表单默认编码也是UTF-8但如果请求头没声明Content-Type的charset容器就用平台默认编码解码中文就乱了。解决在回调接口第一行设置req.setCharacterEncoding(GBK)。如果你用的是Spring MVC可以在配置类里注册CharacterEncodingFilter强制请求和响应都走GBK。注意只对回调接口设GBK不够下单时拼接的报文字段在发送前做GBK的URL编码两处必须一致。5.3 现象所有请求都报“证书错误”或“获取私钥失败”本地联调正常部署到测试服务器后签名工具类抛异常提示找不到私钥。原因证书在本地用的是绝对路径比如C:\cert\merchant.pfx部署到Linux服务器后这个路径不存在。或者证书文件是放在WebContent/WEB-INF/classes/下但打包时被maven过滤掉了。解决证书路径不要用绝对路径改成classpath:加载或者放在一个和项目配置文件同级的config/目录通过配置项指定。如果证书要被多个模块引用更稳妥的方式是把证书内容读成字节流后写到一个临时文件再用KeyStore.load加载避免文件路径问题。InputStream certStream PayConfig.class.getClassLoader().getResourceAsStream(cert/merchant.pfx); KeyStore ks KeyStore.getInstance(PKCS12); ks.load(certStream, password.toCharArray());5.4 现象银行返回“0000”成功但商户订单没入库银行侧显示交易成功流水也能查到但商户数据库里的订单还是“待支付”。原因异步通知到达时接收接口处理能力不足或出现了异常。比如落库代码里查了不存在的外部服务抛了运行时异常导致接口直接返回500银行收到非success响应会重发但你这边每次重发都在同一个位置抛异常最终银行重发到上限放弃。解决回调接口必须保证“先验签后落库异常也要吃掉并返回success”。更稳妥的做法是收到通知后先写一张通知流水表任何异常都记录到流水里再异步处理真正的业务更新。这样即使业务处理失败事务可以回滚但通知已经被记录下来不会丢消息。5.5 现象支付成功后浏览器没有跳回商户页面用户付款成功页面停在银行的结果页或白屏没有回到商户商城。原因同步跳转ReturnURL配置不可达。最常见的是回调地址写成了http://localhost:8080/return用户在自己的电脑上付款成功后银行服务器把浏览器重定向到localhost:8080指向的是用户本机什么服务都没有自然白屏。解决ReturnURL必须是公网可达的域名或IP。更隐蔽的问题是HTTP和HTTPS混用如果银行网关是HTTPS回调地址写成了HTTP部分银行会拒绝跳转。检查这两点同步跳转问题基本能解决。6. 进阶用法把demo升级成能上线用的版本最后一章说一个能把demo改造成生产级别的具体技巧以及一个验证支付完整性的习惯性动作。先把demo里缺的东西补齐。第一是幂等。回调接口里的markPaid要先查后更并且用数据库唯一索引兜底。具体做法是给订单流水表加order_id唯一约束重复通知到达时插入失败捕获异常后仍然返回success或repeat。第二是日志。支付请求和回调要打印完整的报文、签名值和验签结果不要只打印业务字段——排查问题时“Signature值对不对”是第一个要确认的日志里没有就抓瞎。具体技巧是写一个“签名自测工具”放在demo里作为独立main方法。它的作用是输入明文、私钥路径、密码输出签名值再输入同样的明文、签名值、银行公钥输出验签结果。用这个工具在正式联调前可以自己先验证签名算法、编码方式、字段拼序是否正确不用等银行接口联调时间。public class SignSelfTest { public static void main(String[] args) throws Exception { String plainText MerchantID103310000000000OrderIDTEST001 Amount100ProductNametestReturnURLhttp://localhost/return NotifyURLhttp://localhost/notify; // 第一步签出签名值 String sig RSAUtils.sign(plainText, D:/cert/merchant.pfx, 123456); System.out.println(签名值: sig); // 第二步验签 boolean ok RSAUtils.verify(plainText, sig, PayConfig.bankPublicKey); System.out.println(验签结果: ok); } }运行这个工具如果验签结果一直是false问题一定在拼接顺序或编码方式而不是在网络上。它能帮你把网络问题从本地问题上剥离开。从那以后我每次接入这类网银接口都会强制自己走一遍“签名自测→下单→回调落库→对账”的完整链路先在本地拿到一笔成功的测试流水再交付联调。宁可花一个小时搭自测工具也不要让银行侧的外部门同事看你反复提交错误请求。这个习惯帮我避开了至少十次联调返工希望帮到你。本文还有配套的精品资源点击获取