ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

农行Web端网银支付Java接口:签名、回调与避坑实战

农行Web端网银支付Java接口:签名、回调与避坑实战 简介农行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.id103123456000001 merchant.pfx.path/WEB-INF/conf/merchant.pfx merchant.pfx.passwordyourCertPwd merchant.cer.path/WEB-INF/conf/abc.cer gateway.pay.urlhttps://payment.abchina.com.cn.example/ebus/Pay gateway.query.urlhttps://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里负责签名的方法把里面的拼接逻辑抄下来而不是自己重新设计。一个常见的支付请求签名示例是这样的// 构造支付请求参数示意代码字段顺序以农行文档为准 MapString, 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.EntryString, String entry : params.entrySet()) { html.append(input name\).append(entry.getKey()) .append(\ value\).append(htmlEscape(entry.getValue())) .append(\/); } html.append(/form); html.append(scriptdocument.payForm.submit();/script); // 在Servlet里输出html response.setContentType(text/html;charsetGBK); 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); MapString, 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); ListNameValuePair pairs new ArrayList(); for (Map.EntryString, 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-charsetGBK同时设置response.setContentType(text/html;charsetGBK)。如果使用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逻辑收进一个PayServicedemo里的支付逻辑散在JSP和Servlet里直接拿到web项目里会到处都是农行参数。我一般会把demo中的配置读取、签名、请求发送、回调验签四件事封装成一个PayService。核心方法只有两个buildPayForm(order)负责生成支付表单handleNotify(request)负责验签和更新订单。这样改造后控制器里不会出现任何农行签名逻辑后续换其他支付通道也更容易。6.2 上线验证清单与自测命令上线前一个小时我建议按下面这个清单逐项过一遍。在测试环境用一分钱商品发起支付确认能从农行收银台完成支付并收到回调。支付完成后立刻调用一次主动查询接口确认查询结果与回调一致。把回调接口临时改成返回错误观察农行是否重发确认不出重复入账。用GBK编码传一个中文订单描述确认收银台显示不乱码。停掉应用模拟回调丢失重启后主动查询能否兜底更新订单。最后把这个接口的流程图画在代码注释里方便下一个维护的人。我第一次接这个接口时把签名串顺序按自己的习惯排死在“报文格式错”上两天后来发现demo就是最权威的参照实现自己造轮子只会更坑。做老接口对接规矩比创意重要老老实实跟着demo走再按业务需求做减法就能少踩很多坑。希望帮到你。本文还有配套的精品资源点击获取
返回列表