ARTICLE DETAIL

资讯详情

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

农行Web端网银支付Java接口实战:从Demo到上线的签名验签与踩坑指南

农行Web端网银支付Java接口实战:从Demo到上线的签名验签与踩坑指南 简介农行Web端网银支付Java接口文件与Demo包面向对接农行B2C支付通道的Java开发人员涵盖接口文档解读、参数组装、签名验证与回调处理等完整流程适用于电商平台及在线服务的支付模块集成。压缩包共147个文件以class编译类、jsp页面、jar依赖库、html说明页为主含properties配置与cer、truststore等安全证书整体约5.1MB。已有2072人学习下载。内含MerchantPara参数封装、SignService签名服务、DataVerifier数据验签等支付核心类并附TrustPay.cer数字证书便于联调测试与二次开发。运行Demo可掌握交易请求构造、数字签名、响应解析、异常处理与回调验证等关键环节缩短集成周期。1. 为什么农行web端网银支付Java接口文件及demo能让一个订单系统卡住一星期接到农行web端网银支付Java接口文件及demo后很多团队的第一反应是“照着demo跑起来就行了”结果往往在支付下单和回调验签两个环节各卡两三天。问题不在农行接口有多难而在于农行的支付报文是“半私有”格式签名串怎么拼、证书从哪读、回调怎么验demo里都有但demo是银行内部的代码风格直接拿进Spring Boot工程会踩一串雷。这篇文章会从接口包里的文件讲起到最小可运行的下单与回调代码再到生产环境必须处理的参数和坑帮你把农行web端网银支付从“能跑demo”推进到“敢上线”也适合第一次接银行支付的Java开发照着一步步做。2. 拆开农行接口包文件构成、证书与三套环境的对应关系2.1 接口包里的文件哪些是必须看的哪些只是摆设农行下发的web端网银支付Java接口包常见是一个zip解压后目录大致如下文件/目录用途必须处理程度接口说明文档PDF或DOC定义报文字段、签名算法、回调字段必须逐字读尤其是“待签名串”章节merchant/商户私钥证书PFX/P12商户端签名用的私钥本地保存必须安全存放农行公钥证书CER验证农行回调签名用的公钥必须随环境切换demo/src演示代码通常是Servlet或JSP版本必须读懂核心两处下单签名、回调验签demo/lib农行封装jar如果有建议先不用优先用标准JCE测试环境地址与文档网银网关URL、测试商户号必须写入配置很多开发者拿到包后先看demo源码这没错但最优先的应该是接口说明文档里的“签名与验签说明”。农行B2C网银支付的老接口和新接口在字段拼法上有差异demo代码里写的是当时联调通过的一套不一定适配你拿到的测试环境版本。我一般会先用文本比对工具把demo里涉及签名的字符串拼接区域高亮出来再和说明文档里的公式对齐。2.2 商户公私钥、农行公钥和测试环境证书的对应关系农行web端网银支付用的是双向签名商户下单时用商户私钥做RSA签名农行用商户公钥验签农行回调时用农行私钥做签名商户用农行公钥验签。因此你手里至少有两份证书材料商户私钥证书通常一个PFX/P12文件里面既有商户公钥也有商户私钥。Java加载时需要读取密码密码在农行邮件或光盘说明书里注意区分测试密码和生产密码。农行公钥证书一个CER文件用来验农行回调。测试环境和生产环境的农行公钥不是同一个不能混用。证书文件的编码也值得注意。PFX是PKCS12格式Java可以直接用KeyStore.getInstance(PKCS12)加载。CER可能是DER或Base64编码用CertificateFactory.getInstance(X.509)读取时如果报“无效的DER编码”需要先判断文件头是不是“-----BEGIN CERTIFICATE-----”是的话就是Base64可先忽略换行再转成字节流。3. 在Java工程里跑通农行网银支付demo从下单表单到同步跳转3.1 组装支付请求待签名串的拼接规则是第一个分水岭农行web端网银支付的下单流程本质是后端生成一段订单数据用商户私钥签名然后拼一个HTML表单让浏览器自动POST到农行网关。农行网关展示支付页面用户完成支付后农行把结果同步发送到returnUrl浏览器跳回和异步发送到notifyUrl服务器通知。待签名串的拼接规则是整个环节里最容易出错的地方。不同版本字段不同常见参考是moneytype-0001v_amount-金额v_mid-商户号v_oid-订单号字段名用英文减号连接值和标识多个字段之间用连接且顺序固定。实际必须以你拿到的农行接口文档为准。demo的价值就在这里demo里的常量拼接顺序就是测试环境验证通过的顺序。不要自己调整顺序也不要图省事把所有字段都拼进去。3.2 用Java写一个支付下单的servlet生成自动提交表单下面是一个不依赖农行SDK、只用JDK标准库的最小下单示例适合放到任何Java Web工程里// PayOrderServlet.java WebServlet(/pay/order) public class PayOrderServlet extends HttpServlet { private static final String GATEWAY_URL https://pay.abchina.com/...; // 以农行文档为准 private static final String MERCHANT_PRIVATE_KEY_PWD test_pwd; // 测试环境密码 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // 1. 准备订单数据 String v_mid 商户号; // 农行分配的商户号 String v_oid System.currentTimeMillis() ; // 订单号 String v_amount 100.00; // 支付金额单位是元 String v_moneytype CNY; String returnUrl https://yourdomain.com/pay/return; String notifyUrl https://yourdomain.com/pay/notify; // 2. 按文档规定的顺序拼接待签名串 String plainText moneytype- v_moneytype v_amount- v_amount v_mid- v_mid v_oid- v_oid; // 3. 加载商户私钥证书并签名 String sign signWithMerchantKey(plainText); // 4. 拼HTML并输出 resp.setContentType(text/html;charsetutf-8); PrintWriter out resp.getWriter(); out.println(form idpayForm methodpost action GATEWAY_URL ); out.println(input typehidden namev_mid value v_mid /); out.println(input typehidden namev_oid value v_oid /); out.println(input typehidden namev_amount value v_amount /); out.println(input typehidden namev_moneytype value v_moneytype /); out.println(input typehidden namev_md5info value sign /); out.println(input typehidden namereturnUrl value returnUrl /); out.println(input typehidden namenotifyUrl value notifyUrl /); out.println(scriptdocument.getElementById(payForm).submit();/script); out.println(/form); } }这里的逻辑说明分三点一是待签名串里不能有订单商品描述等可变动字段农行只校验你提交签名对应字段的原始值二是v_md5info只是HTML表单里的name具体叫什么以农行文档为准有的版本叫signMsg三是自动提交用document.getElementById(payForm).submit()不要用带确认弹窗的写法避免用户取消导致交易卡死。签名方法实现如下使用Java标准JCEprivate String signWithMerchantKey(String plainText) throws IOException { try { // 1. 读取PFX证书 KeyStore ks KeyStore.getInstance(PKCS12); ks.load(new FileInputStream(/path/to/merchant.pfx), MERCHANT_PRIVATE_KEY_PWD.toCharArray()); String alias ks.aliases().nextElement(); PrivateKey priKey (PrivateKey) ks.getKey(alias, MERCHANT_PRIVATE_KEY_PWD.toCharArray()); // 2. SHA1withRSA签名部分新版本用SHA256withRSA以文档为准 Signature signature Signature.getInstance(SHA1withRSA); signature.initSign(priKey); signature.update(plainText.getBytes(UTF-8)); // 3. Base64编码后返回 return Base64.getEncoder().encodeToString(signature.sign()); } catch (Exception e) { throw new IOException(merchant sign failed, e); } }参数说明PKCS12不能写成JKS否则打开PFX会抛异常aliases().nextElement()取第一个别名正常情况下PFX里只有一个私钥条目getBytes(UTF-8)与农行网关的字符集必须一致很多验签失败就是这里用默认平台编码Windows下是GBK导致。签名算法名建议先看农行PDF里写的RSA摘要算法如果是SHA1就写SHA1withRSA如果是SHA256就换成SHA256withRSA两者不互通。3.3 接收农行同步回调验签与订单更新农行在用户支付完成后会将支付结果POST到returnUrl。这个请求里带着订单号、金额、以及农行用私钥做的签名。必须在处理订单状态前先验签否则任何人都能伪造“支付成功”的请求。// PayReturnServlet.java WebServlet(/pay/return) public class PayReturnServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { String v_oid req.getParameter(v_oid); String v_amount req.getParameter(v_amount); String signMsg req.getParameter(signMsg); // 或v_md5info以文档为准 // 1. 拼出待验签字符串顺序与农行约定一致 String plainText moneytype-0001v_amount- v_amount v_mid- req.getParameter(v_mid) v_oid- v_oid v_status- req.getParameter(v_status); // 2. 用农行公钥验证 boolean ok verifyWithBankPublicKey(plainText, signMsg); if (ok) { // 3. 验签成功后检查金额与订单号是否匹配你的库 // 然后更新订单状态为“已支付” resp.getWriter().write(success); } else { resp.getWriter().write(verify fail); } } private boolean verifyWithBankPublicKey(String plainText, String signMsg) throws IOException { try { CertificateFactory cf CertificateFactory.getInstance(X.509); Certificate cert cf.generateCertificate(new FileInputStream(/path/to/bank.cer)); Signature signature Signature.getInstance(SHA1withRSA); signature.initVerify(cert.getPublicKey()); signature.update(plainText.getBytes(UTF-8)); return signature.verify(Base64.getDecoder().decode(signMsg)); } catch (Exception e) { throw new IOException(verify bank sign failed, e); } } }这里的关键不是把代码贴上去而是要理解两步先验签再核对订单金额和商户号。验签通过只说明这个请求确实来自农行不代表这单就是你们的有效订单必须用v_oid查出本地订单比对v_amount是否一致金额不一致时宁可终止也不要改状态。同步回调是浏览器跳转回来的用户可能中途关闭页面导致农行收不到响应所以真正的入账确认要依赖notifyUrl的异步通知同步回调只适合做页面展示。4. 把demo放到真实Web项目里多环境切换、字符集与金额单位的三个必调参数4.1 测试环境与生产环境的配置切换农行给的demo通常把测试地址写死在代码里比如https://easyay.95599.cn这类地址具体以文档为准。上线前必须把网关URL、商户号、证书路径改成生产配置。我建议用配置中心或至少一个PayProperties类把这些值收拢ConfigurationProperties(prefix abchina.pay) public class PayProperties { private String gatewayUrl; private String merchantNo; private String privateKeyPath; private String privateKeyPassword; private String bankPublicKeyPath; private String returnUrl; private String notifyUrl; // getter/setter 省略 }这样application.yml里可以按profile切换# application-test.yml abchina: pay: gateway-url: https://test-pay.abchina.com/... merchant-no: 111222333 private-key-path: classpath:cert/test/merchant.pfx private-key-password: test#2026 bank-public-key-path: classpath:cert/test/bank_test.cer切换环境时只动配置不动代码。注意证书不要放到git仓库的公开目录最好跳过版本控制由部署系统在启动时从加密存储里拉到指定路径。4.2 金额单位、字符集、编码格式为什么总对不上农行网银支付的金额字段在不同接口版本有不同约定有的用“元保留两位小数”有的用“分”且在拼待签名串时不能带逗号分隔符。demo里通常会写DecimalFormat(0.00)但如果你直接传100.0得到的字符串可能是100.0而不是100.00签名字符串就变了。我的处理方式是统一用BigDecimal先格式化String v_amount new BigDecimal(100.00).setScale(2, RoundingMode.HALF_UP).toString();字符集方面农行网关历史上对中文参数支持并不友好。订单号、回调地址里尽量避免中文下单表单页统一输出charsetutf-8读回调参数时也明确指定UTF-8。如果工程里某个过滤器强制设置GBK务必在支付接口的路径上排除掉否则验签时getBytes(UTF-8)和农行侧编码不一致就会翻车。4.3 证书加载的两种方式文件路径与KeyStoredemo最常见的是每次请求时用文件流加载PFX。这在低并发下没问题但支付下单接口往往会遭遇秒杀流量高频读文件加解析证书会拖慢响应。建议启动时把PrivateKey和PublicKey加载到内存全局复用。把第3章的signWithMerchantKey改造为一次性初始化public class PayCrypto { private final PrivateKey merchantKey; private final PublicKey bankKey; public PayCrypto(String pfxPath, String pwd, String bankCerPath) { this.merchantKey loadMerchantKey(pfxPath, pwd); this.bankKey loadBankKey(bankCerPath); } public String sign(String plainText) { /* 直接用merchantKey */ } public boolean verify(String plainText, String signMsg) { /* 直接用bankKey */ } }注意私钥对象不是线程安全的Java的Signature实例也不是。可以用ThreadLocal包装或者直接用Apache Commons Crypto这类并发安全的库。最省事的做法是给签名方法加synchronized支付下单并发量通常不需要极致优化但至少不会抛“Signature not initialized”之类的内存错误。5. 农行网银支付接入的高频坑与排查验签失败、回调丢失、页面乱码5.1 回调验签一直失败看看是不是多了空格或换行现象postman直接模拟农行回调验签返回false但农行技术说他们的测试工具能通过。原因拼接待验签字符串时从request取出的参数值可能带着HTML编码比如被转成amp;或者属性文件里写的签名串拼接模板首尾多了个空格。另一个常见点是从Fiddler抓包复制签名时Base64字符串被换行截断导致Base64解码后字节不完整。解决先在验签方法里把plainText和signMsg两侧的空白trim掉同时打印出实际参与验签的字符串用十六进制或者JSON格式输出到日志和农行demo控制台打出来的日志逐位对比。重点对比v_amount是否有多余的.0以及拼好的串里字段间使用的连接符是不是。5.2 支付成功但订单状态没变同步回调和异步回调的时序关系现象用户支付完成回到商户页面页面显示“支付成功”但后台订单还是待支付。等几分钟后状态才变或者始终不变。原因你把订单更新逻辑写在同步回调returnUrl里但returnUrl在浏览器跳转时如果被页面刷新、跳转去广告页农行还没等到响应就断了也有的情况是同步回调里验签失败但页面显示成功因为用户已经付了款农行侧则认为商户不需要确认。解决把订单状态更新只放在notifyUrl异步通知里不要把returnUrl当作业务收银台。异步通知里验签成功后用数据库幂等键比如订单号唯一索引做INSERT ... ON DUPLICATE KEY UPDATE这样农行重发通知时不会重复改状态。具体做法可以看第6章的幂等示例。5.3 页面显示乱码、金额多了一分现象跳转农行页面后商品名显示乱码或者支付成功后回调里金额变成“100.999999999”。原因商品参数用了GBK编码农行页面用了UTF-8金额传递过程中走了double类型浮点精度被放大。解决不要传中文商品名到农行网关农行的字段说明里没要求的话一律省略金额在拼参数前用BigDecimal格式化到两位小数。回调里拿到v_amount后先用字符串解析再转成BigDecimal禁止用Double.parseDouble。5.4 JDK版本换了老demo突然跑不起来现象农行demo在三年前的JDK8环境能通过换到JDK11或17后在加载证书时报NoSuchAlgorithmException或者javax.xml.bind包缺失。原因老demo依赖JAXB做报文解析JDK9以后JAXB被移出默认JDK另外某些RSA算法在JDK高版本下要求更长的密钥或者显式启用的Provider。解决优先不用农行老jar包改用标准JCE写签名验签就是第3章的方法如果你的账户文件是1024位RSA密钥生产环境建议联系农行升级到2048位部分测试网关只认旧密钥这是一个很容易被忽略的“兼容性”坑。我见过有团队因为密钥位数太短在新JDK上验签偶尔失败后来在文档里找到原因找银行换证书才彻底解决。5.5 Fiddler抓包抓不到农行回调链路是HTTPS加直接POST现象本地联调时用Fiddler想抓农行回调请求结果只看到一串无法解密的CONNECT隧道或者回调根本不到本地。原因农行回调是服务器到服务器的POST如果你本地工程是通过花生壳或Nginx暴露的农行服务器请求的是公网地址不经过你本机Fiddler代理。另外农行网关是HTTPSFiddler需要安装并信任根证书才能解密。解决做notifyUrl本地调试时用内网穿透工具把本机端口暴露成公网地址再把notifyUrl配置成这个公网地址。抓包时看农行有没有发送成功可以在内网穿透日志里观察POST path如果穿透工具不支持HTTPS就在自己的Web服务器前面加Nginx把证书终止掉先用HTTP验证业务逻辑再把证书恢复。正式环境必须HTTPS不能因为调试方便就留明文回调。6. 从demo到可上线的支付模块日志、幂等与最后的自测清单6.1 用一行SQL守住回调幂等性农行异步通知没有严格的“只发一次”保证正常情况会按间隔重试。订单表设计时把支付平台订单号作为唯一键通知处理时先插入通知流水再更新订单INSERT INTO pay_callback_log (v_oid, abchina_oid, amount, sign_msg, create_time) VALUES (?, ?, ?, ?, NOW()); -- 如果insert成功说明第一次通知继续业务 -- 如果duplicate key说明重复通知直接返回success这里v_oid是商户订单号如果不允许同一订单重复处理唯一键就建立在v_oid上。插入成功后事务里再更新订单状态更新语句带条件WHERE statusPENDING保证状态只能从待支付改成已支付不能从已支付退回。最后无论是否重复都要返回success农行收到success才停止重发返回其他内容会继续重试。6.2 本地伪造农行回调来验证验签逻辑生产环境不能随意测测试环境又未必同步数据建议写一个本地JUnit函数用农行测试环境私钥如果有给你的本地回调接口发请求做自测。注意这里不是让你用商户私钥测试环境里农行回调签名用的私钥一般不会给商户所以可行的函数式验证是把验签方法单独抽出来先用一个已知的“农行demo里附带的签名样例”跑通再mock一个回调请求Test public void testVerifyWithSampleData() { PayCrypto crypto new PayCrypto(test_merchant.pfx, pwd, bank_test.cer); String ret moneytype-CNYv_mid-...; // 用农行demo日志里的sample String sign ...; // 同一条日志里的签名串 assertTrue(crypto.verify(ret, sign)); }把验签方法做成无副作用、只依赖输入的纯函数这比抓实际回调包更利于自动化回归。每次农行更新证书或环境切换后先跑这个测试能省下大量手工联调时间。6.3 上线前根据这份清单自查检查项预期结果生产网关URL和测试网关URL是否配置在正确profile登录页不出现测试域名生产商户号是否与证书匹配下单后农行页面显示商户名称正确私钥证书和生产密码是否一致签名不报错、验签通过商户下单金额格式化两位小数无逗号异步通知幂等重复通知订单状态不变日志是否记录待签名串明文调试时可以看但生产建议脱敏超时与重试下单接口超时后前端有明确提示最后一个习惯是上线前我会故意在notifyUrl里删除证书跑一次模拟回调确认系统不会因为异常导致订单卡死而是有日志和告警。农行网银支付接口的接入难点从来不在代码量而在“签名串必须和文档完全一致”这种黑匣子规则上。希望这篇文章的踩坑记录能帮你在接支付时少熬几个夜也祝你的订单闭环一次跑通。本文还有配套的精品资源点击获取
返回列表