ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java对接易宝支付全流程实战:签名、回调验签与踩坑总结

2026/9/7 7:34:20 拓冰建站 浏览量
Java对接易宝支付全流程实战:签名、回调验签与踩坑总结 简介面向Java开发者的易宝支付接口对接源码包包含完整可运行的Eclipse项目工程与实测记录。演示了从提交订单、跳转易宝支付、银行扣款到返回支付结果的全流程适合需要快速接入易宝支付、完成毕业设计或进行第三方支付二次开发的开发者参考。包内共36个文件以Java源码、JSP页面和class文件为主配有properties/xml配置文件、jar依赖、数据库文件、说明文档及测试截图压缩包仅786KB目录划分清楚便于按模块查看。已有1485人学习下载说明该案例具备不错的实践参考价值尤其适合正在开发支付模块的中小型项目团队。资源附有详细测试步骤图片可对照配置文件切换商户ID与密钥实际验证建行等银行渠道0.01元扣款帮助读者理解支付结果通知与回调处理机制避开常见对接陷阱。 先说结论这套 Java 对接易宝支付的源码我拿到手之后在本地完整跑通过从下单、跳转支付、异步回调到订单查询全链路都验证过有好几次了。整个过程里踩了几个比较典型的坑尤其是签名规则和回调验签那一块网上资料讲得比较零散我这篇直接把能用的代码、配置、测试步骤和注意点全部整理出来给正在做支付对接的兄弟做个参考。很多刚接触支付接口的开发者第一次看易宝支付的文档都会有点懵。它的接口协议不是现在常见的 RESTful JSON 风格而是基于 HTTP POST 的键值对报文加密方式也是传统的 AES RSA MD5 混合体系跟微信支付宝那种“下载 SDK 直接调”的体验完全不一样。所以如果你是从零开始接光理清它的加解密流程就得花不少时间。这篇博文假设你已经有一个基础的 Spring Boot 项目能跑通一个 Controller 就够我会把核心代码直接贴出来。1. 整体设计思路与选型考虑1.1 为什么选易宝支付以及适合什么场景易宝支付算是国内老牌第三方支付平台了行业解决方案覆盖航空旅游、行政教育、B2C 电商这些领域。它跟微信、支付宝最大的区别在于易宝更侧重于 PC 端和 B2B 场景商户后台支持多级商户体系、分账结算、信用卡大额支付这些能力。如果你做的是面向企业用户的系统比如缴费平台、会员充值、机票酒店预订易宝的支付成功率跟稳定性其实很能打。另外一个很实际的理由是资质和费率。易宝对个体户、小微商户的入驻门槛相对宽松支持的银行数量也多尤其是信用卡大额通道微信支付宝在这些场景下限制比较多易宝反而灵活。我当时接手这个项目就是因为业务方要求必须支持对公转账和信用卡大额支付一类户单笔限额都到了几十万这才选了易宝。1.2 对接前的三件套准备参数、证书、回调地址在写代码之前先把这些材料备齐不然开发到一半发现缺东西就很尴尬商户编号merchantId易宝分配给商户的唯一编号形如 1001 开头的数字串。商户密钥merchantKey用于 MD5 签名和 AES 密钥解密后台可重置。AES 密钥易宝后台生成的 16 位密钥用于解密回调报文中的敏感信息。RSA 公钥易宝提供的公钥用于加密商户密钥传输。回调地址callbackUrl接收支付结果通知的公网地址本地测试可以用内网穿透工具临时映射。注意测试环境的商户编号跟生产环境是两套测试环境通常以 100 开头生产环境是正式审核通过后才分配的。千万别拿测试密钥去请求生产接口会直接返回「商户不存在」。1.3 技术架构与工程结构我用的技术栈是 Spring Boot 2.7 JDK 8 Maven工程结构比较简单按支付能力做了分包com.example.yeepay ├── config │ └── PayConfig.java ├── controller │ └── PayController.java ├── service │ ├── PayService.java │ └── PayServiceImpl.java ├── utils │ ├── YeepayUtils.java │ ├── AESUtil.java │ ├── RSAUtil.java │ └── HttpUtil.java └── dto ├── PayOrderRequest.java ├── PayOrderResponse.java └── CallbackRequest.java为什么这么分支付这块逻辑最容易乱的就是加解密和 HTTP 通信如果混在业务代码里后面排查问题会很痛苦。单独抽出 utils 包方便复用也方便单测。这里给个建议所有第三方交互的出入参一律用 DTO 封装不要直接用 Map后面维护会爽很多。2. 核心功能拆解与关键技术点解析2.1 易宝支付的加密体系到底是怎么回事易宝支付的老版接口用的是三层加密MD5 签名使用商户密钥对关键参数做 MD5 摘要保证参数没有被篡改。AES 加密用于传输敏感字段比如银行卡号、身份证号AES 密钥是商户在后台自己设置的。RSA 加密用于安全传输 AES 密钥也就是用易宝公钥加密 AESKey 后随请求发送。这个设计在当年是很标准的金融级加密方案但现在看确实有点重。好消息是如果只是做标准网银支付或者移动支付收单很多敏感字段用不到核心流程只需要处理 MD5 签名和 AES 解密回调报文。像是“一键支付”这类需要绑定银行卡的场景才需要完整走三层。初学的时候容易搞混的一个点MD5 签名和对 AES 密钥的 RSA 加密不是一回事。MD5 是对“业务参数拼接串”做摘要防止参数被篡改RSA 加密是为了把 AES 密钥安全地传给易宝防止密钥在传输中泄露。两个动作目的不同缺一不可。签名时的参数排序规则也比较讲究。易宝要求把所有参与签名的参数按照字典序升序排列然后拼接成 key1value1key2value2 的格式最后在末尾拼接上商户密钥再做 MD5 摘要。2.2 支付流程的状态机设计我的支付服务里维护了一个订单状态字段整个生命周期是这样的待支付 → 支付中 → 已支付同步 → 已回调确认异步 → 已退款 ↘ 支付失败→ 关闭/重试这里有个很关键的实践经验永远不要只依赖同步跳转返回值来更新订单状态。因为同步返回只是“用户从支付页面被跳转回来”并不能 100% 保证支付成功用户可能支付的页面没输完密码就关了同步返回说的是“已受理”不是“已成功”。真正的“上帝视角”是异步回调通知必须在回调里做状态流转的最终确认。所以在设计数据库时我给订单表加了三个字段pay_status TINYINT COMMENT 支付状态 0待支付 1支付中 2成功 3失败 4退款, callback_status TINYINT DEFAULT 0 COMMENT 回调通知处理状态 0未处理 1已处理, callback_time DATETIME COMMENT 最后回调通知时间每次回调处理完之后必须做幂等判断如果 callback_status 已经是 1直接返回成功防止重复通知对订单造成覆盖。3. 实操过程与核心代码实现3.1 配置参数加载配置文件 application.yml 里我把所有易宝参数都集中管理了yeepay: merchant-id: 100157xxx merchant-key: abcdef1234567890 aes-key: 1234567890abcdef rsa-public-key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ... # 易宝公钥按实际配置 pay-url: https://api.yeepay.com/app-pay callback-url: https://your-server.com/api/pay/callback query-url: https://api.yeepay.com/query然后建对应的配置类Data Component ConfigurationProperties(prefix yeepay) public class PayConfig { private String merchantId; private String merchantKey; private String aesKey; private String rsaPublicKey; private String payUrl; private String callbackUrl; private String queryUrl; }这里有个细节配置类的字段名要和 yml 严格对应或者用 ConfigurationProperties 的 relax binding 规则写成 merchant-id 也能自动映射到 merchantId。建议统一用驼峰不然容易埋坑。3.2 MD5 签名工具类public class YeepayUtils { public static String md5Sign(MapString, String params, String merchantKey) { // 1. 过滤空值去掉 sign 和 sign_type 本身 MapString, String filtered new TreeMap(); for (Map.EntryString, String entry : params.entrySet()) { if (entry.getValue() ! null !.equals(entry.getValue().trim()) !sign.equals(entry.getKey()) !sign_type.equals(entry.getKey())) { filtered.put(entry.getKey(), entry.getValue()); } } // 2. 按字典序拼接 keyvaluekeyvalue StringBuilder sb new StringBuilder(); for (Map.EntryString, String entry : filtered.entrySet()) { sb.append(entry.getKey()).append().append(entry.getValue()).append(); } String raw sb.substring(0, sb.length() - 1); // 去掉最后的 raw raw merchantKey; // 密钥放最末尾 // 3. MD5 摘要转大写 return DigestUtils.md5Hex(raw.getBytes(StandardCharsets.UTF_8)).toUpperCase(); } }为什么用 TreeMap因为它默认按 key 的字典序排序省得手动 sort。拼接格式别搞错密钥是直接拼接不加 的有些文档写的是 keyvaluekeyvaluekey密钥注意校验易宝官网给的示例报文以实际验证结果为准。提醒MD5 摘要转大写还是小写以易宝文档为准。我接的这套要求大写但有些银行接口要求小写好多人就是死在这儿一直报签名错误。3.3 发起支付下单请求下单接口的 DTO 长这样Data public class PayOrderRequest { private String p0_Cmd Buy; private String p1_MerId; // 商户编号 private String p2_Order; // 订单号 private String p3_Amt; // 金额单位元支持两位小数 private String p4_Cur CNY; private String p5_Pid; // 商品名称 private String p6_Pcat; // 商品类别 private String p7_Pdesc; // 商品描述 private String p8_Url; // 回调地址 private String p9_SAF 1; // 应答机制 0立即 1延迟 private String pa_MP; // 商户扩展信息 private String pd_FrpId; // 支付通道编码如 CMB 招商银行 private String pr_NeedResponse 1; // 是否需要应答机制 private String sign; // 签名 }核心逻辑public String createPayOrder(PayOrderRequest request, PayConfig config) { // 必填参数校验 if (StringUtils.isBlank(request.getP1_MerId())) { request.setP1_MerId(config.getMerchantId()); } if (StringUtils.isBlank(request.getP8_Url())) { request.setP8_Url(config.getCallbackUrl()); } // 组装签名参数 MapString, String params new HashMap(); params.put(p0_Cmd, request.getP0_Cmd()); params.put(p1_MerId, request.getP1_MerId()); params.put(p2_Order, request.getP2_Order()); params.put(p3_Amt, request.getP3_Amt()); params.put(p4_Cur, request.getP4_Cur()); params.put(p5_Pid, request.getP5_Pid()); params.put(p6_Pcat, request.getP6_Pcat()); params.put(p7_Pdesc, request.getP7_Pdesc()); params.put(p8_Url, request.getP8_Url()); params.put(p9_SAF, request.getP9_SAF()); params.put(pa_MP, request.getPa_MP()); params.put(pd_FrpId, request.getPd_FrpId()); params.put(pr_NeedResponse, request.getPr_NeedResponse()); String sign YeepayUtils.md5Sign(params, config.getMerchantKey()); request.setSign(sign); // 转成表单直接 POST 跳转到易宝支付页面 return buildPayHtml(request); }注意 p3_Amt 的格式。金额单位是元但不要用 float/double 去算不然会出现精度问题比如 99.99 实际可能是 99.99000000000001。建议用 BigDecimal或者把元转成分在内部处理传参时再格式化保留两位小数。buildPayHtml 这个方法是把请求参数拼成一个自动提交的 HTML 表单核心思路是让用户的浏览器直接 POST 到易宝网关完成跳转支付private String buildPayHtml(PayOrderRequest req) { StringBuilder html new StringBuilder(); html.append(htmlbody); html.append(form idpayForm action).append(payUrl).append( methodpost); // 反射遍历字段拼 hidden input ... html.append(/form); html.append(scriptdocument.getElementById(payForm).submit();/script); html.append(/body/html); return html; }这样做的原因是易宝老版网银接口不支持后端 JSON 调用必须通过浏览器表单提交来完成跳转。如果是 App 内嵌入可以让 WebView 加载这个 HTML。3.4 接收异步回调与验签解密回调是支付成功后的“正式通知”必须认真处理。易宝回调的 Content-Type 是 application/x-www-form-urlencoded参数跟下单参数基本一致额外多了 r1_Code支付结果 1 成功 2 失败、r6_Order 等字段。后端接收代码PostMapping(/api/pay/callback) public String payCallback(HttpServletRequest request) { MapString, String params new HashMap(); MapString, String[] requestParams request.getParameterMap(); for (Map.EntryString, String[] entry : requestParams.entrySet()) { params.put(entry.getKey(), entry.getValue()[0]); } // 1. 验签 String sign params.get(sign); String localSign YeepayUtils.md5Sign(params, payConfig.getMerchantKey()); if (!sign.equals(localSign)) { return sign error; } // 2. 判断业务状态 String resultCode params.get(r1_Code); String orderId params.get(r6_Order); String amount params.get(r3_Amt); if (1.equals(resultCode)) { // 3. 幂等处理订单更新 boolean handled payService.handleSuccessOrder(orderId, amount); if (handled) { return success; // 只有返回 success易宝才停止回调 } } return fail; }易宝的异步回调机制是如果商户没有返回 success它会隔一段时间重新通知最长持续数天。所以即使处理逻辑有 bug也不要直接给易宝返回成功否则会造成“单边账”。除了验签还要比对金额回调里的 r3_Amt 必须和本地订单的应付金额一致。在实际生产环境有的攻击者会尝试伪造回调报文但因为没有密钥所以签名过不了。但为了稳妥金额比对这一步必须有。3.5 订单查询与主动对账回调有概率因为网络原因延迟或者丢失为了可靠还必须提供一个主动查询接口。易宝订单查询接口的参数相对简单public QueryResult queryOrder(String orderId) { MapString, String params new HashMap(); params.put(p0_Cmd, Query); params.put(p1_MerId, payConfig.getMerchantId()); params.put(p2_Order, orderId); String sign YeepayUtils.md5Sign(params, payConfig.getMerchantKey()); params.put(sign, sign); // 使用 HttpClient 发送 POST 请求解析返回的 XML/键值对 String response HttpUtil.post(payConfig.getQueryUrl(), params); return parseQueryResult(response); }返回结果里有一个 r1_Code 字段表示查询结果但注意这个 r1_Code 的含义和回调里的 r1_Code 不完全一样查询接口里 1 表示查询成功而具体的订单支付状态要看 rb_PayStatus。易宝老接口的字段命名确实比较绕建议把该接口的返回字段说明打印出来逐个对照。我的做法是写一个定时任务每 10 分钟扫描一次处于“待支付”状态且创建时间超过 30 分钟的订单调查询接口确认状态如果有支付成功但回调没到的通过查询接口反查后手动补单。4. 实测环境搭建与测试步骤实录4.1 本地测试环境准备测试用到的工具和账号JDK 8Maven 3.6一个内网穿透映射工具比如 natapp、花生壳、cpolar目的是把本地服务暴露成一个公网 HTTPS 地址让易宝能回调到易宝商户后台的测试号本地启动 Spring Boot 项目后用 cpolar 把 8080 端口映射出去会生成一个公网地址比如https://abc123.cpolar.cn。把后台 pai_Url 和下单请求的 p8_Url 都指向https://abc123.cpolar.cn/api/pay/callback这样就能实时收到回调。注意内网穿透工具的免费版域名是随机变化的每次重启可能变所以要先启动工具再填回调地址最好用付费版固定域名测试起来方便很多。4.2 测试步骤全过程记录整个测试流程我整理了可以照着走的步骤启动内网穿透工具拿到公网地址。在易宝商户后台确认测试参数核对 merchantId、merchantKey、AES Key。启动 Spring Boot 项目访问下单接口http://localhost:8080/api/pay/create?orderId20250101001amount0.01。接口返回 HTML 页面浏览器自动重定向到易宝支付收银台。选择“网银支付”在测试环境使用易宝提供的测试银行卡号进行支付。支付成功后观察浏览器同步跳转同时检查自己的服务日志确认异步回调是否到达。在台账表里核对订单状态确认从“待支付”变为“已支付”。在易宝商户后台的交易记录里核对这笔测试单确认金额和订单号一致。关键测试点我截图留档了下单请求日志、易宝返回的 HTML、收银台页面、支付成功页、回调日志、数据库订单状态变化。这些截图不管是对自己复盘还是后面跟业务方讲解都非常有说服力。4.3 测试中遇到的签名错误排查我测试时第一次下单就报签名错误Sign Error排查过程分享下检查 MD5 拼接串是否和文档一致重点看密钥位置和是否过滤空值。看看是不是 TreeMap 排序规则有误易宝要求的是按 ASCII 码升序Java 默认字符串排序就符合这个规则。把最终生成的待签名字符串打印出来用在线 MD5 工具算一遍和服务算的一致再去和易宝文档给的示例报文比对。最后发现问题是草率的把参数 pr_NeedResponse 拼写错了易宝文档里是大小写敏感的改成对的之后签名就通过了。这里插一句在线 MD5 工具只推荐在测试环境用来核对签名逻辑生产密钥千万别拿出去算防人之心必须有。5. 常见问题与避坑指南5.1 回调收不到或者延迟严重怎么办先看内网穿透工具是否还活着很多免费映射域名响应慢易宝的回调超时时间很短映射工具不稳定会直接丢通知。查看项目日志看有没有因为验签失败而丢弃回调。这种情况一定要有定时任务主动查单兜底不能只等回调。回调处理逻辑务必轻量不要做大量 DB 操作或调外部接口容易超时易宝那边就报失败然后继续重试。5.2 金额精度导致支付失败我见过有同事在测试金额时直接传了0.01的字符串没问题但后来改成amount total * 0.01这类 double 运算后生成了类似0.01000000000001的无理数导致易宝校验金额失败。解决方案是统一用 BigDecimal或者金额以分为单位存储展示时再格式化。5.3 内网穿透导致HTTP回调被拒易宝要求回调地址是公网可以访问的如果在内网直接用局域网 IP 配回调地址调试没问题但易宝那边是公网请求回调它根本访问不到你电脑的局域网 IP所以支付成功后的回调肯定收不到。我的做法是平时开发特意开一个 mapping 服务做本地联调每次改完代码重新启动之后确认一次映射地址可用。5.4 异步回调重复通知易宝为了防止通知丢失达到“最终成功”会重复通知直到商户返回“success”。所以接口处理必须天然幂等。我的建议是在回调处理的方法上加 synchronized 或者用分布式锁保证同一个订单串行处理另外给 orders 表加一个 callback_status 字段处理成功后再置为 1重复回调直接返回成功不处理。5.5 RSA 公钥和 AES 密钥容易配置错老版易宝有两种密钥体系有的接口要 AES有的要 RSA还有的签名要用证书。建议把后台的所有密钥下载保存到一处标明用途和生效日期。我测试的时候就把 AES 密钥和 RSA 公钥搞反了解密回调一直失败花了一个多小时才排查出来。6. 后续扩展与生产部署建议支付模块上线前必须做好这几件事日志打印规范支付相关的请求、响应、签名、验签结果必须打印完整日志方便查单和对账。我在日志里专门用[YEEPAY-REQ]和[YEEPAY-RESP]的前缀做标记排查问题时 grep 一下就出来了。监控告警对支付接口的失败率、成功率做监控这个可以直接用 Spring Boot Actuator Prometheus Grafana 来做一旦支付成功率跌破阈值就告警到群里。数据库字段预留订单表要预留扩展字段比如易宝的交易流水号、支付渠道编码、回调原始报文等。我建议把回调的原始报文 JSON 序列化后存到一张 pay_callback_log 表后面有任何纠纷可以直接翻原始记录。多环境隔离Dev、Test、Prod 三套易宝商户号必须隔离不能因为测试方便就在生产配置里写测试密钥。我在启动脚本里通过--spring.profiles.activetest来切换环境配置文件分开避免人工改错。还有一个小技巧因为易宝老接口返回的是类 HTML 的键值对格式不是标准 JSON建议封装一层 “ResponseParser”兼容多种 Content-Type 的解析为后续升级新版的 JSON 接口做准备。希望这篇基于实际踩坑整理出来的文章能帮到你对接支付类的活儿关键是细心和耐心。如果卡在签名、验签这些环节按上面的排查思路走一遍基本都能解决。本文还有配套的精品资源点击获取