
简介面向Java后端开发者资源包用于打通农行Web端网银支付的Java接口集成链路。适合电商、在线服务等需要接入农行网银支付的团队尤其是在银行对接方面缺少经验的开发者可据此快速理解接口文档降低启动门槛。压缩包共147个文件约5.1MB包含Java接口类与可运行Demo、JSP与HTML示例页面、Jar依赖库、properties配置、cer证书及truststore信任库class文件对应支付与签名核心逻辑页面和配置方便本地部署与模拟联调整体结构清晰便于按模块对照学习。目前已有2072人学习下载。包内演示了交易请求、签名验证、响应处理、异常处理及回调通知等关键步骤结合接口文档可看清农行支付流程的完整链路升级版接口包还附带证书与信任库开发者可直接配置测试环境使用并根据Demo中的商户配置、参数组装与验签逻辑将接口快速迁移到业务系统。1. 农行Web端网银支付的Java接口demo先理解“表单跳转”再动手第一次接触农行Web端网银支付Java接口的人大多被“银企直连”四个字带偏了方向。这份“农行web端网银支付java接口文件及demo”拆开看就是一个标准的前置表单跳转支付场景商户网站生成订单把关键字段用商户私钥签名连同订单信息一起以form表单POST到农行支付网关用户付款完成后农行通过页面跳转和后台通知两个渠道把结果返回商户系统。它跟微信、支付宝那种“调一次API拿支付链接”的模式完全不一样核心工作量集中在证书加载、签名验签、字段拼接、回调幂等这四块。这套demo把这条链路完整拉了一遍适合已经能写Servlet或Spring Boot接口、但对银行加密报文不熟的Java后端也适合需要快速评估农行B2C接入成本的架构师。拆完后的第一感觉是真正容易翻车的不是代码是文档参数表和证书。2. 通信模型与准备为什么这套接口不是REST风格证书与密钥要分清2.1 农行B2C网银支付的通信模型为什么是表单跳转先明确一点农行B2C网银支付不是让商户服务器直接请求农行接口拿支付链接而是把订单信息放到HTML表单里通过用户浏览器跳转到农行支付页面。这个过程里商户服务器做的事情有且只有两件一是为订单信息签名二是输出一段自动提交的表单。用户输完支付密码后农行把结果同步返回到商户的ReturnURL页面跳转同时异步发通知到NotifyURL服务器后台。这两个回调是分开的页面跳转是给用户看结果的异步通知才是商户系统真正用来改订单状态的信号来源。之所以用这种“笨重”的方式是因为银行支付网关对安全性和兼容性的要求远超普通互联网接口它不允许商户服务器从后端直接发起扣款请求所有涉及用户身份验证的动作必须在银行页面上完成。同时为了兼容不同商户五花八门的服务器环境报文格式也做得足够简单——一段规整的参数字符串、一个签名字段、一次表单提交。理解了这一点对接时心里就不会老想着“我该怎么拿到支付链接”而是把精力放在“农行需要的字段我有没有拼对、签名有没有通过”上。2.2 证书体系商户私钥、农行公钥、PIN码农行在签约之后会下发一套证书和商户号。商户侧需要保管的东西通常包括格式为PFX或P12的商户证书文件里面是商户私钥用于给请求报文签名格式为CER的农行公钥证书用于验证农行返回结果的签名另外还有证书PIN码加载PFX时要用。测试环境和生产环境的证书是两套不能混用。这一点在后续排错时特别关键——测试环境跑得好好的一换生产证书全链路验签失败多半就是测试证书和正式证书交错配置了。Java加载PFX证书的标准路径是KeyStore.getInstance(PKCS12)再配合CertificateFactory加载CER公钥。demo里通常会把这部分封装成一个证书工具类业务代码不直接碰文件流。注意一点给请求报文签名用的是商户私钥验证农行返回数据用的是农行公钥。这两个方向不能搞反不然现象就是“发送时成功回调验签必失败”。2.3 开发环境准备JDK、Web容器、测试网关这套demo我一般用JDK8跑容器用Tomcat8.5或Spring Boot内嵌Tomcat都可以农行文档里给的示例工程大多是Servlet结构直接扔进Tomcat就能起。你需要准备的内容有JDK8、Tomcat或Spring Boot、农行测试证书一套、农行测试网关地址、一个测试用的外网域名。如果只是本地开发没有外网域名可以用内网穿透工具把本机8080端口映射出去再把ReturnURL和NotifyURL配成映射后的地址。在开始改代码之前建议把农行文档里的“测试环境地址”和“生产环境地址”先找出来分别存到配置里。再强调一次支付网关地址、证书文件、商户号这三样一定要以农行下发的技术文档为准。网上很多帖子贴出来的是旧版地址直接复制进代码大概率会踩“请求打不到网关”的坑。3. 用demo把支付流程跑通配置、签名、提交与回调验签代码拆解3.1 工程结构与配置类先改四个参数农行的demo工程拿下来之后结构上大同小异一个配置类或properties文件、一个签名工具类、若干个处理支付和回调的Servlet。我这里按我习惯的拆法把它整理成四个核心文件MerchantConfig.java配置、SignUtil.java签名验签、PayServlet.java发起支付、NotifyServlet.java接收异步通知。同步回调和异步回调可以共用同一个验签逻辑区别只在于处理后的跳转行为。配置类是最先要动手的文件。我把这个类里需要替换的参数列在下面其他业务相关的配置保持demo原样即可public class MerchantConfig { /** 商户代码农行签约后下发字母数字组合 */ public static final String MERCHANT_ID YOUR_MERCHANT_ID; /** 商户私钥证书路径PFX/P12格式放在resources下或绝对路径 */ public static final String PFX_PATH /config/merchant_test.pfx; /** 证书PIN码农行随证书一起下发 */ public static final String PFX_PASSWORD YOUR_PIN; /** 农行公钥证书路径CER格式用于验证农行返回数据的签名 */ public static final String ABC_CER_PATH /config/abc_test.cer; /** 支付网关地址测试环境和生产环境不同以农行文档为准 */ public static final String PAY_GATEWAY https://pay.abchina.com/...; }这四个参数是这条链路里最容易被改错的。MERCHANT_ID不是营业厅账号而是商户签约后农行分配的支付商户号PFX_PATH必须指向包着私钥的PFX/P12文件单独的CER文件是不含私钥的PIN码是证书口令不是登录密码。至于PAY_GATEWAY我习惯在配置里分“测试”和“生产”两个常量切换环境时只改一处避免上线时漏改地址。3.2 签名工具类SHA1withRSA加签与验签农行这套接口最核心的算法是RSA签名。商户侧用PFX里的私钥对请求报文字符串做签名农行侧用商户公钥验签反过来农行返回数据时用农行私钥签名商户用农行公钥验签。签名算法基本是SHA1withRSA签名结果做Base64编码后放到请求参数里。下面是这个工具类的核心方法public class SignUtil { /** 加载PFX中的商户私钥 */ public static PrivateKey loadPrivateKey(String pfxPath, String password) throws Exception { KeyStore ks KeyStore.getInstance(PKCS12); try (InputStream in new FileInputStream(pfxPath)) { ks.load(in, password.toCharArray()); } String alias ks.aliases().nextElement(); return (PrivateKey) ks.getKey(alias, password.toCharArray()); } /** 加载CER中的农行公钥 */ public static PublicKey loadPublicKey(String cerPath) throws Exception { CertificateFactory cf CertificateFactory.getInstance(X.509); try (InputStream in new FileInputStream(cerPath)) { X509Certificate cert (X509Certificate) cf.generateCertificate(in); return cert.getPublicKey(); } } /** 对报文字符串做SHA1withRSA签名Base64输出 */ public static String sign(String data, PrivateKey privateKey) throws Exception { Signature signature Signature.getInstance(SHA1withRSA); signature.initSign(privateKey); signature.update(data.getBytes(UTF-8)); return Base64.getEncoder().encodeToString(signature.sign()); } /** 验证签名data为原始报文字符串sign为Base64签名字符串 */ public static boolean verify(String data, String sign, PublicKey publicKey) { try { Signature signature Signature.getInstance(SHA1withRSA); signature.initVerify(publicKey); signature.update(data.getBytes(UTF-8)); return signature.verify(Base64.getDecoder().decode(sign)); } catch (Exception e) { return false; } } }这段代码逻辑上不复杂但有两个点要注意。一是“待签名字符串”的编解码必须统一用UTF-8农行网关侧对编码敏感如果项目里其他地方用了GBK签名验不过去时很难定位。二是verify方法里我把所有异常都兜住了并返回false这是故意的——验签失败属于异常分支与其让异常冒泡不如直接返回false业务层只需要关心true还是false。实际生产里我会在false路径上额外打一条日志记录订单号和签名串的前几位方便排查。3.3 发起支付拼参数、做签名、输出自动提交表单支付发起这一步做的事情可以拆成三步构造有序参数按农行文档规定的顺序拼接成待签名字符串签名后把参数和Signature一起放进HTML表单。这里参数顺序是硬性要求农行文档里一般会给一个示例串比如“MerchantIDxxxOrderNoxxxOrderAmountxxx”拼接顺序和字段名要和示例完全一致少一个字段都算验签失败。WebServlet(/pay) public class PayServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { // 1. 订单号商户生成保证唯一农行侧会做重复校验 String orderNo PAY System.currentTimeMillis(); // 2. 订单金额单位分100表示1元 String orderAmount 100; // 3. 商品名称联调阶段先用固定值 String productName 测试商品; // 4. 同步跳转地址支付完成后浏览器跳回来 String returnUrl http://your-domain.com/return.do; // 5. 异步通知地址农行后台通知商户服务器 String notifyUrl http://your-domain.com/notify.do; // 6. 按文档顺序构造待签名串字段名必须与文档一致 String plain MerchantID MerchantConfig.MERCHANT_ID OrderNo orderNo OrderAmount orderAmount ReturnURL returnUrl NotifyURL notifyUrl; // 7. 加载商户私钥做签名 PrivateKey privateKey SignUtil.loadPrivateKey( MerchantConfig.PFX_PATH, MerchantConfig.PFX_PASSWORD); String sign SignUtil.sign(plain, privateKey); // 8. 输出自动提交表单页面加载后立即跳转农行支付网关 resp.setContentType(text/html;charsetUTF-8); PrintWriter out resp.getWriter(); out.println(htmlheadmeta charsetUTF-8); out.println(scriptwindow.onloadfunction(){document.pay.submit()}/script); out.println(/headbody); out.println(form namepay methodpost action MerchantConfig.PAY_GATEWAY ); out.println(input typehidden nameMerchantID value MerchantConfig.MERCHANT_ID ); out.println(input typehidden nameOrderNo value orderNo ); out.println(input typehidden nameOrderAmount value orderAmount ); out.println(input typehidden nameProductName value productName ); out.println(input typehidden nameReturnURL value returnUrl ); out.println(input typehidden nameNotifyURL value notifyUrl ); out.println(input typehidden nameSignature value sign ); out.println(/form/body/html); } }这段代码里OrderAmount用的是“分”这一点容易被忽略农行文档里的单位说明字很小我第一次对接时差点翻车提交10元却显示成0.1元。ProductName要展示中文商品名前端页面已经声明UTF-8的前提下它到底要不要参与签名取决于农行文档的字段约定有的版本只要求签名核心字段有的则要求全部字段参与签名务必以文档里的“签名元素表”为准。输出表单时再强调一遍所有hidden字段的name要和农行文档里的参数名大小写完全一致demo里原有的SignType、PayType这类字段保留默认值就行。3.4 回调验签与订单更新同步和异步两条链路分开处理支付完成后农行会通知商户两种结果同步ReturnURL和异步NotifyURL。同步通知是用户浏览器跳转过来的可能出现用户中途关闭页面导致丢失异步通知才是可靠的最终状态来源。两条链路处理的核心逻辑一样取参数、拼待签名串、验签、查本地订单、更新状态唯一不同的是响应方式——同步要给用户一个HTML页面异步要给农行回写一个固定标识让农行知道通知已收到。WebServlet(/notify) public class NotifyServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding(UTF-8); // 1. 取农行回调参数 String orderNo req.getParameter(OrderNo); String orderAmount req.getParameter(OrderAmount); String sign req.getParameter(Sign); // 2. 按与发起支付一致的顺序拼接待验签串 String plain OrderNo orderNo OrderAmount orderAmount; // 3. 用农行公钥验签 PublicKey publicKey SignUtil.loadPublicKey(MerchantConfig.ABC_CER_PATH); boolean valid SignUtil.verify(plain, sign, publicKey); if (!valid) { // 验签失败记录告警日志不回写成功标识 resp.getWriter().write(FAIL); return; } // 4. 幂等处理先查本地订单状态避免重复通知覆盖 Order order orderService.getByOrderNo(orderNo); if (order null) { resp.getWriter().write(FAIL); return; } if (PAID.equals(order.getStatus())) { // 已经处理过直接返回成功 resp.getWriter().write(SUCCESS); return; } // 5. 校验金额后更新订单状态 orderService.markPaid(orderNo, Long.parseLong(orderAmount)); // 6. 回写成功标识让农行停止重发通知 resp.getWriter().write(SUCCESS); } }这段代码的关键在“先查后改”。异步通知在农行侧有重试机制如果商户服务返回非SUCCESS或超时农行会按间隔重发重发可能来上十几次如果每次到达都直接更新状态后到的旧状态通知会覆盖新状态。所以订单状态更新前先查一次状态是接口幂等性最基础的一道防线。实际项目里我会在数据库订单表上加唯一索引把order_no设为唯一约束双保险。至于回调参数名是Sign还是Signature不同版本文档有差异以你手上那份接口文档为准。4. 避坑记录6个让生产环境翻车的细节4.1 证书加载失败PFX密码报错现象程序启动或第一次发起支付时抛java.io.IOException: keystore password was incorrect。原因最常见的情况有两种一是PIN码和证书文件不匹配测试证书配了生产证书的密码二是KeyStore实例类型不对默认的JKS加载不了PFX文件。解决把KeyStore.getInstance(PKCS12)写死然后用keytool命令验证证书本身没问题keytool -list -v -keystore merchant.pfx -storetype PKCS12输入密码后能看到别名、有效期和证书链。如果命令能通过而代码报错再排查密码是否带了不可见字符。4.2 待签名字符串顺序不一致导致签名错误现象表单提交到农行后农行页面提示签名错误或验签失败订单压根进不了支付页。原因农行对参与签名的字段和拼接顺序有严格约定用了HashMap导致字段输出顺序随机或者字段名大小写不一致。解决严格按农行文档“签名元素表”的顺序拼接代码里用LinkedHashMap保证插入顺序。我一般在联调前先用文档里的示例报文和一个已知的签名结果做一次本地比对判断自己拼出来的待签名字符串是否和文档里字字一致。4.3 中文商品名乱码现象支付成功后商户后台的订单商品名是乱码用户在银行页面上看到的产品名称也乱码。原因项目默认编码是GBK或ISO-8859-1页面输出表单和签名时用的编码不一致导致农行侧看到的字节和商户侧不一致。解决全链路统一UTF-8。JSP或Servlet里显式声明charsetUTF-8需要URL编码的字段如ProductName在拼进待签名串之前先URLEncoder.encode而且签名用的也必须是编码后的值。4.4 异步通知地址配了内网地址支付成功订单不更新现象用户支付成功页面跳转回来了但后台订单一直停留在“待支付”。原因NotifyURL配的是localhost或192.168开头的内网地址农行支付网关根本访问不到。解决NotifyURL必须是一个公网可访问的域名且和支付请求里填的地址保持一致。本地联调时用内网穿透工具把端口暴露出去把NotifyURL配成穿透后的公网域名。注意不要把ReturnURL和NotifyURL配成同一个一个是GET跳转一个是POST通知混在一起容易出乱子。4.5 异步通知重复到达订单状态被覆盖现象订单状态先后出现“已支付”和“已退款”的跳动或同一笔订单在日志里被处理了多次。原因农行异步通知有重发机制商户侧处理时间过长或返回异常农行会重发重发和首次通知并发到达订单状态被后到的报文覆盖。解决处理前先查订单当前状态已终态直接返回SUCCESS订单表加order_no唯一索引更新状态用乐观锁update ... where statusUNPAID影响行数为0时说明已经被处理过直接返回成功。4.6 测试证书和生产证书混用现象测试环境一切正常切到生产环境后全部验签失败。原因测试证书里的商户私钥对应测试环境的农行公钥生产环境用的是另一套根证书签发的证书两套密钥对完全不匹配。解决上线切环境时把商户PFX、农行公钥CER、网关地址三个配置一起换掉。我习惯把测试和生产环境各放一份properties启动时通过profile加载从根上避免手工改配置时漏换文件。最后这条值得多说一句如果你在测试环境是用“农行测试公钥”验签的那么生产环境不仅要换商户PFX还要换农行的生产公钥CER这俩经常有人漏换其中一个。5. 从demo到生产先做本地自签自验再换正式证书在正式联调之前建议先跑一遍“本地自签自验”用例。所谓自签自验就是用商户私钥对一段测试字符串签名再用农行公钥去验证这段签名。如果验证通过说明证书加载、签名算法、Base64编解码、编码格式整条链路都是通的。这一步完全不依赖农行网关能非常快地把证书类问题隔离掉省得后面联调时对着日志猜来猜去。Test public void testSignAndVerify() throws Exception { PrivateKey privateKey SignUtil.loadPrivateKey( MerchantConfig.PFX_PATH, MerchantConfig.PFX_PASSWORD); PublicKey publicKey SignUtil.loadPublicKey( MerchantConfig.ABC_CER_PATH); String plain MerchantIDTESTOrderNoPAY20240101001OrderAmount100; String sign SignUtil.sign(plain, privateKey); boolean result SignUtil.verify(plain, sign, publicKey); // 如果result为false先检查证书文件是否配套再检查编码 Assert.assertTrue(result); }联调时按这个顺序走先启动本地服务配好测试证书然后发起一笔金额为1分钱的测试订单走完整个支付流程。等待异步通知到达后看服务端日志里是否打印了“验签通过、订单已更新”的记录。同步回调和异步通知的时序可以观察正常情况下农行先发页面跳转再发后台通知时间差通常不到一秒。如果只收到了同步没收到异步优先级最高的排查项就是NotifyURL的可达性其次是验签是否失败被拦住了。生产上线前拿下面这张表逐项过一遍检查项要求商户PFX证书生产证书PIN码正确农行公钥CER生产环境公钥与商户证书配套支付网关地址生产网关非测试地址ReturnURL公网HTTPS域名与页面跳转需要匹配NotifyURL公网HTTPS域名可POST接收金额单位分确认订单金额与支付金额一致幂等控制唯一索引和状态判断已加上日志脱敏不打印签名串明文和证书PIN码这八项里日志脱敏是我自己吃过一次亏才补上的。一次排查问题把完整签名串打到了日志里事后想想如果日志被拖走等于把支付接口最关键的信息暴露给了别人。从那以后每次对接网银支付我都会在代码评审时强制过一遍这张检查清单签名串只打前8位和后4位PIN码一律不允许出现在日志和配置文件里。这套农行Web端网银支付的Java接口文件和demo如果你能照着这个顺序把证书链路先验证一遍基本就不会在联调阶段被“签名错误”这四个字反复折磨希望帮到你。本文还有配套的精品资源点击获取