拓冰建站拓冰建站
首页 / 资讯中心 / 正文

农行BRIDGE商户直连Java DEMO:签名验签与证书配置实战

简介面向需要接入中国农业银行缴费中心的Java开发人员这份BRIDGE新版商户直连DEMOV1.4提供了完整的支付对接示例解决商户系统与农行缴费中心之间订单处理、支付确认、退款、回调通知等接口联调问题。压缩包共133个文件以57个Java源码、44个JSP页面为主辅以接口调用所需的JAR依赖、XML配置、JS脚本、properties配置及CER/PFX安全证书文件整体大小6.92MB目录结构清晰便于按模块查阅与复用。已有566人学习下载。Java文件覆盖核心业务逻辑与API调用JSP呈现商户后台交互页面XML与properties定义环境参数证书文件用于联调环境的安全认证配套的V1.4接口文档则详细说明URL、请求参数、响应格式与错误码帮助开发者快速理解农行BRIDGE新版商户接入流程。通过学习和运行DEMO可直接复用支付请求、结果验签、异常退款等关键代码缩短真实业务系统与农行缴费中心的对接开发周期。1. 中国农业银行缴费中心 BRIDGE 商户直连JAVA 版 V1.4 DEMO 到底能帮你省哪些事中国农业银行缴费中心的 BRIDGE 商户直连 DEMOJAVA 版本V1.4应该是不少接缴费业务的团队最先拿到的参考工程它配合那套接口文档解决的是把自家业务系统对接到农行缴费中心这件事。无论是水电燃气、学费党费还是非税缴费走商户直连都意味着用户不用跳出你的 App 或公众号去农行页面付款支付结果由你的后端直接接收。这个 DEMO 把证书装载、报文组装、签名验签、HTTP 调用和回调解析串成了一条可运行的链路。但它不是拿来就能跑的玩具工程我见过太多同事栽在证书格式和签名串上。这篇笔记给准备动手的人讲讲怎么把它读懂、跑通、避坑。2. BRIDGE 直连与缴费接口模型先看懂链路再碰代码2.1 BRIDGE 在农行缴费体系里是什么角色BRIDGE 这个名字在农行缴费中心的技术体系里指的是商户接入的桥接网关。农行内部的缴费主机、核心系统、分行业务系统之间链路很复杂对商户来说不可能直接暴露内网接口所以 BRIDGE 作为统一出入口接收商户系统发来的 HTTPS 请求转发给缴费中心再把处理结果同步返回。链路大致是商户后台服务器 → HTTPS 加密通道 → BRIDGE 网关 → 农行缴费中心主机 → 返回同步应答。异步环节则是缴费完成后由缴费中心主动向商户的回调地址推送结果通知。所以对接时你实际要处理两种流量一种是主动查询和缴费下单的请求-响应另一种是被动接收的支付结果回调。接入模式是否需要证书用户体验开发工作量适用场景商户直连BRIDGE 直连需要双向认证支付全程在商户渠道内完成较大需处理签名、加密、回调自有 App、公众号、小程序、PC 官网跳转农行缴费页面一般不需要用户跳出商户渠道较小几乎零开发无技术团队的轻量接入或低频缴费直连模式的核心价值在于渠道可控和体验可控这也是为什么很多商户宁可多花两周开发时间也要接直连。而 BRIDGE 网关屏蔽了农行内部接口差异商户只需要跟 BRIDGE 定义的报文格式打交道不必关心农行主机侧的细节。这个 DEMO 就是围绕这套报文格式给出的 Java 参考实现。2.2 DEMO V1.4 的工程结构和需要先读的两份文档拿到 DEMO 压缩包后第一件事不是急着在 IDE 里打开跑而是先看目录结构。常见做法是里面会有 src 目录存 Java 源码、resources 目录存配置文件和证书样例、doc 目录存接口文档。V1.4 这批文档相比旧版通常会把接口规范和 DEMO 使用说明拆得更细内容也更多乱翻很容易迷失。我给新人的建议是按下面这个顺序读。文档或交付物主要用途建议阅读顺序DEMO 使用说明或 README讲工程怎么导入、配置怎么改、跑通最小流程第 1 个读20 分钟建立全局观商户直连接口规范 V1.4报文结构、字段定义、签名规则、错误码第 2 个读配合代码对照证书与密钥说明商户私钥、银行公钥的格式和加载方式第 3 个读否则证书坑够你踩一天版本变更说明或升级记录本次 V1.4 相比旧版的字段和接口变化老项目迁移时才需要重点看读文档的时候不要从头到尾啃要先找到三样东西报文结构图或字段表、签名流程说明、一个完整的请求报文样例。DEMO 代码是围绕这些文档实现的你拿着样例报文去代码里找对应的字段组装会很快建立感觉。这个工程的 Java 部分通常是一个 Maven 工程依赖农行提供的 SDK 工具包和常见 HTTP、JSON 库。IDE 导入后直接运行主函数往往会失败因为配置文件里的商户号和证书路径是示例值需要替换成农行给你的测试商户资料。V1.4 相比旧版常见差异是字段增补和报文样例更新具体以包内变更说明为准但证书加载和签名验签的核心逻辑一般不会大变。3. 用 DEMO 跑通第一笔缴费请求证书装载与测试环境配置3.1 配置文件里要动的四个关键项DEMO 工程的 resources 目录下基本都会有一个配置文件可能是 properties 也可能是 yml作用都是把和运行环境相关的参数抽出来。你要改的核心就是四样东西商户号、BRIDGE 地址、证书路径、回调地址。下面这份配置是我按常见工程结构还原出来的模板字段名可能和你的包内有差异但含义一致。merchant: id: 123456789012 # 农行分配的 12 位商户号测试环境和生产环境不一样 name: 测试商户 bridge: url: https://{环境域名}/brd/gateway.do # 测试环境地址由农行对接人员提供 connect-timeout-ms: 5000 # 连接超时默认 5 秒够用跨境或弱网可放宽到 10 秒 read-timeout-ms: 15000 # 读取超时缴费下单接口一般不会超过 15 秒 security: pfx-path: classpath:cert/merchant_test.pfx # 商户私钥证书PKCS12 格式 pfx-password: ${PFX_PASS} # 证书密码别硬编码到代码里走环境变量 public-key-path: classpath:cert/abc_public.cer # 农行公钥证书用于验签 sign-type: SHA256withRSA # 签名算法以文档说明为准 callback: url: https://{商户域名}/pay/callback/abc # 农行异步通知的接收地址这里有个血泪经验证书密码千万不要明文写在 yml 里提交到 Git 仓库。因为配了测试证书密码一不小心就跟着代码一起进了版本库后面生产证书密码如果复用等于把生产通道的钥匙交出去了。我一般用环境变量注入如上文的${PFX_PASS}部署时在服务器的环境变量里配置既避免泄露也方便多环境切换。改完配置别急着跑。先确认一件事农行给你的测试证书是 PFX 还是 JKS 格式公钥是 CER 文件还是 Base64 字符串。V1.4 的 DEMO 如果兼容多种格式加载代码里一般会有一个 KeyLoader 之类的工具类根据扩展名自动选择加载方式。如果你的证书格式和 DEMO 默认不一致优先改配置文件而不是改代码。3.2 从 Client 类跟到请求发送-验签-解析的完整链路配置就位后从 DEMO 的主入口或单元测试进入你会看到一个封装好的 Client 或 Service 类里面按顺序完成了五件事组装报文、生成签名、发送请求、验签、解析应答。不要把这五步拆散因为农行侧验签时要求收到的字段必须与签名时完全一致你在哪一步多塞了个空字段或少带了字段后面全是验签失败。下面是一段按常见实现整理的伪码结构参考了这类直连 DEMO 的标准写法。// 1. 组装业务参数用 TreeMap 保证 key 按 ASCII 升序排列 TreeMapString, String params new TreeMap(); params.put(trxId, generateTrxId()); // 商户流水号必须唯一 params.put(merId, config.getMerchantId()); params.put(orderAmt, fenToString(amount)); // 金额以“分”为单位转字符串 params.put(payType, 01); // 缴费类型用前端传值 params.put(billNo, bill.getBillNo()); // 账单号 params.put(callbackUrl, config.getCallbackUrl()); // 2. 用商户私钥对待签串签名签名结果放回参数里 String signSrc SignUtil.buildSignSrc(params); // 按“kvkv”拼接剔除空值 byte[] signBytes SignUtil.sign(signSrc.getBytes(StandardCharsets.UTF_8), privateKey); params.put(sign, Base64.getEncoder().encodeToString(signBytes)); // 3. HTTPS 发送到 BRIDGE 网关 String respBody HttpClientUtil.postForm(config.getBridgeUrl(), params); // 4. 解析响应先验签再取业务数据 TreeMapString, String respMap parseForm(respBody); boolean ok SignUtil.verify( SignUtil.buildSignSrc(respMap), // 注意去掉响应里的 sign 字段再拼 respMap.get(sign), publicKey); if (!ok) { throw new VerifyException(农行响应验签失败); }这段代码里几个关键设计值得留意。第一用TreeMap而不是HashMap因为它天然按 key 做字典序排列省得自己写比较器签名串的排序规则通常就要求 ASCII 升序。第二金额转成“分”字符串而不是用 double避免浮点精度把一笔 0.1 元的订单变成 0.10000000001 元。第三验签时要从响应 Map 里先把sign字段摘掉再拼接否则等于带着签名去验签名。参数层面的超时设置也在这里体现。connect-timeout管的是 TCP 连接建立read-timeout管的是发完请求后等响应的时间。农行缴费中心偶尔会因为批处理任务变慢把 read-timeout 设到 15 秒以上能减少误报失败的次数但也别设太长否则你的线程池容易被慢接口占满。失败重试要配在业务层而不是 HTTP 层。跑通流程后你会收到同步应答里面包含处理结果码、银行流水号和缴费状态。但千万别把同步应答当作最终结果缴费类接口常常是异步确认的最终状态要等回调通知或主动查询接口来确认。这就是下一章要说的签名验签细节也是直连接入里最容易出问题的环节。4. 签名验签与数据加解密RSA 参数怎么设才不会跑不通4.1 签名算法的选择与密钥格式PFX、CER、Base64 之间是什么关系农行缴费中心这类银企直连接口签名算法常见的是 RSA 系具体签名算法名在 DEMO 配置里写成SHA256withRSA即 SHA-256 做摘要、RSA 做签名密钥长度一般要求 2048 位。商户用自己的私钥对请求报文签名农行用商户公钥验签反过来农行用自己的私钥对响应和通知签名商户用农行公钥验签。全程是单向签名不是双向加密。这里有个常见的概念混淆值得展开讲。PFX 文件里装的是商户私钥和商户证书加载时要用 PKCS12 的 KeyStore 类型CER 文件是农行公钥证书用来验农行发来的内容。你签名需要用 PFX 里的私钥你验签需要用 CER 里的公钥两者不能搞混。我见过有人拿着商户的 CER 去验农行的通知结果自然是消息认证码不匹配。// 加载 PFX 中的商户私钥这是签名用的钥匙 KeyStore keyStore KeyStore.getInstance(PKCS12); try (InputStream in new FileInputStream(pfxFile)) { keyStore.load(in, password.toCharArray()); } String alias keyStore.aliases().nextElement(); // 证书别名一般在证书里可见 PrivateKey privateKey (PrivateKey) keyStore.getKey(alias, password.toCharArray()); // 加载农行公钥证书这是验签用的钥匙 CertificateFactory cf CertificateFactory.getInstance(X.509); X509Certificate bankCert; try (InputStream in new FileInputStream(cerFile)) { bankCert (X509Certificate) cf.generateCertificate(in); } PublicKey bankPublicKey bankCert.getPublicKey();代码里值得注意的点有两个。第一KeyStore.getInstance(PKCS12)不要随手写成JKS那是 JDK 默认的老格式用 JKS 加载 PFX 会直接抛异常或得到空别名。第二从 KeyStore 取 key 时要传密码这个密码和登录 KeyStore 的密码通常一致但也不绝对个别分行发的测试证书有过别名密码和整体密码不同的情况真遇到报错keystore password was incorrect时先查这一点。部分 V1.4 的包会附带国密分支支持 SM2/SM3 的签名算法算法名形如SM3withSM2依赖里会多一个 BouncyCastle 的 provider。如果你的对接分行要求国密DEMO 里一般会有单独的配置开关把sign-type切过去即可。国密不是每家分行都启用先看文档里有没有《国密算法说明》没有就老老实实走 RSA别自己加戏。签名算法的参数里还有一项容易被忽略字符集。农行侧对报文用的编码是 UTF-8你组装待签串时用getBytes(UTF-8)不要依赖平台默认字符集。代码里我会显式写StandardCharsets.UTF_8而不是省事写getBytes()否则在 Windows 本机是 GBK服务器上是 UTF-8同一段代码签名结果不同验签必挂。4.2 签名串的组装顺序和验签失败的“玄学”90% 的问题出在待签串我接触过不少接直连的团队反馈验签失败时第一反应都是“密钥不对”“算法不对”实际上九成案例的根因在待签串。待签串的组装规则是农行接口规范里写得最严格的部分常见要求是字段按 key 的 ASCII 码升序排列、值为空或 null 的字段不参与签名、拼接时用keyvalue并用分隔、结尾不带。public static String buildSignSrc(MapString, String params) { // 去掉 sign 本身和所有空值TreeMap 自动升序 TreeMapString, String sorted new TreeMap(); for (Map.EntryString, String e : params.entrySet()) { if (e.getKey() null || sign.equals(e.getKey())) { continue; } String v e.getValue(); if (v ! null !v.isEmpty()) { sorted.put(e.getKey(), v); } } StringBuilder sb new StringBuilder(); for (Map.EntryString, String e : sorted.entrySet()) { sb.append(e.getKey()).append().append(e.getValue()).append(); } // 去掉末尾多出来的那个 这是最常见的翻车点 if (sb.length() 0) { sb.deleteCharAt(sb.length() - 1); } return sb.toString(); } }这段代码里有一个隐藏问题如果某个 value 本身含有或字符拼接出来的待签串会被拆分后重新组装导致农行侧还原不出来。解决办法是在文档允许的前提下对 value 做 URL 编码再拼接且必须对 key 和 value 同时采用同一套编码规则。但注意这不是绝对标准农行各分行接口对编码处理并不完全一致有的要求不编码直接拼以你拿到的接口规范为准。代码里我会把这种处理做成常量开关而不是写死在拼接逻辑里。验签失败的排查路径我会按照下面的顺序走一遍。第一步把待签串原样打印出来逐字符核对是否多了空格、换行或制表符第二步确认签名时用的密钥是商户私钥、验签时用的密钥是银行公钥第三步确认签名结果经过了 Base64 编码后再放入报文有些 DEMO 返回的是 hex 字符串格式错了验签必然失败。这三步走完九成问题能定位。剩下的玄学问题多半是复制报文时从 PDF 里带进了不可见字符或 IDE 自动给文件加了 BOM 头。至于报文体本身的加解密这类直连接口里常见做法是业务字段明文加 RSA 整体签名部分敏感字段如手机号、身份证号会在业务层做 AES 加密后放入报文。V1.4 的 DEMO 里如果带了加密工具类八成是把 AES 密钥放在配置里和 PFX 同级管理。这个密钥属于对称密钥泄露比证书泄露后果更直接建议走配置中心或云上密钥管理不要和代码一起打包。5. 常见问题排查BRIDGE 直连 DEMO 调试里最容易翻车的 5 个点5.1 证书加载阶段的三个坑格式、别名、密码各说一次现象 1运行 DEMO 主程序控制台直接抛java.io.IOException: keystore password was incorrect但密码确认过是对的。原因PFX 文件的 KeyStore 密码和私钥条目密码不一致。多数证书工具导出 PFX 时会让你设置两级密码第一级保护整个 KeyStore第二级保护私钥条目两个密码可以不同。DEMO 代码里通常只提供一个密码字段用它 load 了 KeyStore 之后再用同一个密码去 getKey 就会失败。解决先用 KeyStore Explorer 之类工具查看 PFX 的私钥条目密码是否与 KeyStore 登录密码一致。不一致时把两个密码分别配置到 yml 的两个字段里一个叫pfx-password一个叫key-passwordDEMO 若没支持就在 KeyLoader 里小改一下。现象 2KeyStore 加载成功但签名时抛出InvalidKeyException: IOException: ObjectIdentifier[] -- Invalid key。原因商户私钥可能不是 RSA 密钥而是 EC 或 SM2 密钥但你签名算法仍设成了 SHA256withRSA。V1.4 的包如果支持多种证书格式pom 里会引入多个加密 provider加载代码要按密钥类型选择算法。解决用工具查看 PFX 里的私钥算法类型。如果是 EC签名算法改成SHA256withECDSA或文档指定的算法如果是 SM2用SM3withSM2并确认 BouncyCastle provider 已注册。现象 3自测时验签通过连到农行测试环境后全部验签失败连错误码都一样。原因测试环境和生产环境的商户号、证书是两套DEMO 默认配置指向生产证书但 BRIDGE 地址却改成了测试地址。农行测试网关持有的商户公钥和你本地私钥不匹配。解决核对三件套商户号、证书对、BRIDGE 地址必须来自同一个环境。最稳的做法是在配置文件名上加环境后缀application-test.yml和application-prod.yml彻底分开避免手工改来改去改漏一个字段。5.2 签名与回调阶段的两个坑待签串里藏了看不见的字符现象 4代码逻辑和文档完全一致但农行返回9999验签失败把打印出来的待签串贴到文档示例里对比肉眼看不到任何差异。原因肉眼看不见的字符在作怪常见三种来源。一是从 PDF 接口文档里复制样例时带入了换行符\n或回车符\r二是 IDE 自动给属性文件加了 UTF-8 BOM 头BOM 字符\uFEFF混进了第一行配置的 value 里三是 Windows 下getBytes()用了 GBK中文字段名转出来的字节序列和 UTF-8 完全不同。解决把待签串用 Base64 编码后打印转成可见字符串再检查开头和结尾。BOM 问题用十六进制查看器确认配置文件首字节是否为EF BB BF是的话用file命令转成无 BOM 格式。字符集问题把代码里所有getBytes()都改成getBytes(StandardCharsets.UTF_8)。现象 5回调通知能收到但验签失败排查发现通知里的签名是用农行私钥生成的你用商户公钥去验了。原因回调通知的签名方向是农行 → 商户必须用农行公钥验签而我们日常调试签名时用的是商户私钥签、农行公钥验。方向搞反的典型特征是自己拼的请求验签通过农行主动推的通知验签必挂。解决在代码里把验签入口拆成两个方法一个叫verifyBankResponse用配置里的农行公钥一个叫verifyCallback同样用农行公钥但实现不同——回调通知里可能还带一个签名原文字段是农行把报文按自己的规则拼好的直接用那个字段拼接验签不要再自己组装。另外把回调地址配到农行测试系统时注意内外网地址映射农行从外网访问不到你在本机的 localhost得用内网穿透或部署到测试服务器上收通知这不算技术难点但很容易到联调当天才发现。回调还有个幂等问题农行通知机制大概率会重发商户系统处理时要按trxId或银行流水号做去重否则同一笔缴费用户被扣了两次确认。把去重表建好用唯一索引兜底这是生产上线前必须做的事。6. 联调验证与生产上线的两个实用技巧多留一分日志少跑一夜对账6.1 把 DEMO 的日志切到 debug 级让签名串和验签结果直接可见很多 DEMO 默认日志级别是 info只打印请求返回码签名串这种关键中间量全被藏掉了。联调阶段第一步就是改日志级别把所有涉及签名验签的包切到 debug。logger namecom.yourcompany.pay.abc levelDEBUG/ logger namecom.abchina.sdk levelDEBUG/切到 debug 之后每次请求至少能看到三行关键日志组装好的待签串、签名后的 Base64 值、农行响应的待验串和验签结果。这三行日志留着出问题时有后悔药可吃。我一般会在生产环境保留这个级别的日志但按天滚动保留七天占不了多少磁盘换来的排查能力非常值。6.2 上线前留一个按日对账的兜底任务直连缴费最怕的不是接口报错而是静默掉单——用户扣了钱你的系统没收到通知。回调会重发但重发也有间隔极端情况网络故障超过重试窗口单子就丢了。所以在生产上线前我会基于 DEMO 里的账单查询接口或交易流水查询接口做每日对账。每天凌晨拉取农行侧前一日全部缴费流水和本地订单表逐笔比对金额一致且状态一致的归档有差异的进人工处理表。// 定时任务示意每天 01:30 执行 Scheduled(cron 0 30 1 * * ?) public void dailyReconcile() { ListBankBill bankBills demoClient.queryYesterdayBills(); for (BankBill bill : bankBills) { Order order orderMapper.selectByTrxId(bill.getTrxId()); if (order null || !order.getAmount().equals(bill.getAmount())) { reconcileMapper.insertProblem(bill); // 有差异进人工池 } } }这个任务代码量不大但能把最后的风险兜住。我第一次接这类直连项目时就是漏了回调重试窗口这个问题上线第二天就有三笔订单用户扣款成功而系统显示未支付客服电话被打爆后来补了对账任务才踏实。日志留足、对账兜底这两件事比任何加密算法都更能保证生产安全。希望帮到你。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门