☰
Java易宝支付对接最小闭环:可运行、可调试、可替换的Spring Boot实战源码
2026/10/15 17:22:26 网站建设 项目流程

简介:本资源是一套完整可运行的Java Web端易宝支付集成源码,面向Java Web开发初学者与支付功能实践者,解决第三方支付接口对接中的环境配置、参数组装、回调处理等典型问题。压缩包共36个文件,含5个核心Java业务类、5个Java工具类、3个JSP前端页面(含登录与支付跳转页)、2个properties配置文件(含商户ID与密钥配置)、2个XML配置文件(Web容器与安全配置)、2个JAR依赖库,以及测试说明文档(.docx)和建行支付成功验证截图(.jpg),整体786KB,结构清晰、轻量易部署。已有1490人学习下载,资源已通过本地Tomcat环境实测,提供完整测试步骤与支付结果URL解析示例,读者可直接复现从用户选择银行、提交订单到跳转易宝支付页并完成0.01元模拟扣款的全流程,附带关键参数含义注释与HMAC签名验证逻辑,助快速掌握支付网关集成核心要点。

1. 这不是SDK封装包,而是一套可直跑、可调试、可替换的Java支付对接最小闭环

你手头正赶一个企业级后台系统,需要嵌入第三方支付通道,但官方Java SDK文档稀碎、示例残缺、报错信息像黑匣子——这时候,一份“已测试成功、带步骤截图”的易宝支付源码,就不是锦上添花,而是救命稻草。它不是抽象的API调用Demo,而是一个完整跑通「下单→签名→跳转→异步通知→验签→状态回写」全链路的Spring Boot工程,含真实商户号配置模板、沙箱环境适配开关、日志埋点位置标注,甚至把易宝回调URL里那个容易被Nginx吞掉的/notify路径都做了反向代理兼容处理。适合两类人:一是刚接手支付模块的后端新人,能照着截图改3个参数就看到支付页弹出来;二是需要快速验证签名逻辑或排查验签失败的老手,源码里把RSAUtil.sign()和RSAUtil.verify()的每一步输入输出都打了DEBUG日志。它不解决高并发削峰,也不做资金对账,但它把易宝支付最让人翻车的5个环节——密钥加载方式、时间戳格式、UTF-8编码强制、回调Body原始流读取、验签时的换行符处理——全钉死在代码里。


2. 源码结构拆解:从入口到回调,每个包名都在说“这里别乱动”

2.1 工程骨架与核心依赖:为什么只选spring-boot-starter-web + commons-codec?

这个源码基于Spring Boot 2.3.12.RELEASE构建,未引入Spring Cloud或Dubbo,纯粹轻量级Web服务。关键依赖只有三项:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.15</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.69</version> </dependency>

提示:易宝RSA签名必须用Bouncy Castle提供SHA256withRSA算法实现,JDK原生SunRsaSign在部分JDK 8u202+版本中会因Provider优先级问题导致验签失败。commons-codec用于Base64编码/解码,避免使用JDK 8的java.util.Base64(其MIME模式默认换行,易宝验签严格校验无换行Base64)。

项目目录结构极简,聚焦支付主干:

src/main/java/com/example/epay/ ├── config/ // 易宝配置类:YeePayConfig.java(含沙箱开关、密钥路径、商户号) ├── controller/ // 两个核心接口:PayController.java(下单)、NotifyController.java(回调) ├── service/ // 核心业务:PayService.java(组装请求参数+签名)、NotifyService.java(验签+状态更新) ├── util/ // 加密工具:RSAUtil.java(签名/验签)、HttpUtil.java(POST请求封装) └── dto/ // 数据对象:PayRequest.java、NotifyRequest.java(字段与易宝文档严格对齐)

config/YeePayConfig.java是整个工程的“开关面板”,所有可配置项集中在此:

@ConfigurationProperties(prefix = "yee.pay") @Component @Data public class YeePayConfig { private String merchantNo; // 商户号(必填) private String privateKeyPath; // 私钥文件路径(如: classpath:rsa_private_key.pem) private String publicKeyPath; // 公钥文件路径(如: classpath:rsa_public_key.pem) private String gatewayUrl; // 网关地址(沙箱:https://test.yeepay.com/...,生产:https://www.yeepay.com/...) private boolean sandbox = true; // 沙箱模式开关(影响网关URL和部分参数值) }

注意privateKeyPath和publicKeyPath必须是类路径资源(classpath:),而非绝对路径。源码中RSAUtil通过getClass().getResourceAsStream()加载,这是为了解决Spring Boot打包成jar后文件不可new File()访问的问题——这是新手第一个血泪坑。

2.2 下单流程:从Controller到签名生成的四层穿透

PayController.createOrder()是支付发起入口,它不做业务校验,只做三件事:接收前端传参 → 构建PayRequest→ 调用PayService生成签名并跳转 → 返回重定向URL。

@PostMapping("/create-order") public ResponseEntity<Map<String, String>> createOrder(@RequestBody PayRequest request) { // 1. 强制设置固定参数(易宝要求) request.setVersion("2.0"); request.setLanguage("zh_CN"); request.setEncodeType("UTF-8"); // 2. 生成唯一订单号(源码用SimpleDateFormat + AtomicInteger模拟,实际应接入雪花ID) String orderNo = "ORD" + System.currentTimeMillis() + new Random().nextInt(1000); request.setOrderNo(orderNo); // 3. 调用服务层完成签名与参数组装 Map<String, String> signedParams = payService.buildSignedParams(request); // 4. 返回重定向URL(前端用window.location.href跳转) String redirectUrl = yeePayConfig.getGatewayUrl() + "/index.do"; return ResponseEntity.ok(Map.of("redirectUrl", redirectUrl + "?" + buildQueryString(signedParams))); }

关键在payService.buildSignedParams()——它不是简单拼接字符串,而是严格按易宝《接口规范V2.0》第4.2节执行:

  1. 将所有非空参数按字典序升序排列(注意:orderAmount是String类型,不是Number,避免JSON序列化丢失前导零);
  2. 拼接成key1=value1&key2=value2&...格式(无空格、无换行、无URL编码);
  3. 对该字符串用商户私钥进行SHA256withRSA签名;
  4. 将签名结果Base64编码(无换行、无填充),赋值给sign字段;
  5. 将sign加入参数Map,返回最终参数集。

buildQueryString()方法手动实现URL编码,而非用URLEncoder.encode(),因为后者会对/、:等字符编码,而易宝网关要求这些字符保持原样:

private String buildQueryString(Map<String, String> params) { return params.entrySet().stream() .map(entry -> entry.getKey() + "=" + entry.getValue().replaceAll("[^\\x00-\\x7F]", "") // 过滤非ASCII字符(易宝不支持) .replace(" ", "%20")) // 仅空格编码 .collect(Collectors.joining("&")); }

参数说明:replaceAll("[^\\x00-\\x7F]", "")是硬性要求。某次实测发现用户姓名含中文“龘”字,导致签名串长度突变,验签直接失败。易宝文档虽未明说,但其验签服务端只接受ASCII字符集。

2.3 回调处理:为什么必须用InputStream读原始Body?

NotifyController.notify()是易宝服务器主动POST过来的异步通知入口。源码没用@RequestBody NotifyRequest notify,而是用HttpServletRequest原始流读取:

@PostMapping(value = "/notify", consumes = MediaType.ALL_VALUE) public String handleNotify(HttpServletRequest request) { try { // 关键:必须用getInputStream()读原始字节流,不能用getParameter() StringBuilder buffer = new StringBuilder(); BufferedReader reader = request.getReader(); String line; while ((line = reader.readLine()) != null) { buffer.append(line); } String rawBody = buffer.toString(); // 1. 解析为Map(易宝回调Body是key=value&key2=value2格式) Map<String, String> notifyMap = parseNotifyBody(rawBody); // 2. 提取sign字段并移除 String sign = notifyMap.remove("sign"); // 3. 对剩余参数按字典序拼接(同下单签名逻辑) String verifyString = buildVerifyString(notifyMap); // 4. 用易宝公钥验签 boolean verified = RSAUtil.verify(verifyString, sign, yeePayConfig.getPublicKeyPath()); if (verified && "1".equals(notifyMap.get("result"))) { // 更新订单状态(此处应调用业务Service) notifyService.updateOrderStatus(notifyMap); return "success"; // 必须返回纯文本success,且无空格换行 } else { log.warn("验签失败或result非1,原始body: {}", rawBody); return "fail"; } } catch (Exception e) { log.error("回调处理异常", e); return "fail"; } }

为什么不用@RequestBody?因为Spring MVC默认会将Body解析为application/x-www-form-urlencoded或application/json,而易宝回调是纯文本key=value格式,且可能含特殊字符。@RequestBody会触发字符集转换(如ISO-8859-1 → UTF-8),导致原始签名串被污染。getInputStream()绕过所有解析层,拿到100%原始字节,这是验签成功的物理前提。


3. 配置与密钥:沙箱环境跑通的六个硬性条件

3.1 易宝沙箱账号申请与密钥生成全流程

要让这份源码真正跑起来,你必须完成以下六步,缺一不可:

  1. 注册易宝沙箱账号:访问易宝开放平台(yeepay.com/open),选择“沙箱环境”,用企业邮箱注册,审核约10分钟;
  2. 下载沙箱证书包:登录沙箱后台 → “开发管理” → “密钥管理” → 下载sandbox_cert.zip;
  3. 提取密钥文件:解压后得到rsa_private_key.pem(商户私钥)、rsa_public_key.pem(商户公钥)、yeepay_public_key.pem(易宝公钥)。源码只需前两个;
  4. 配置商户号与密钥路径:将merchantNo填入application.yml,privateKeyPath和publicKeyPath设为classpath:rsa_private_key.pem;
  5. 开启沙箱模式:yee.pay.sandbox=true,源码会自动切换网关URL为https://test.yeepay.com/...;
  6. 配置Nginx反向代理(若部署在公网):确保/notify路径不被截断或重写,易宝回调必须直达你的应用端口。

注意:沙箱环境的merchantNo是15位纯数字,形如100000000000000,不是生产环境的字母+数字组合。填错会导致“商户不存在”错误,且错误码不提示具体原因。

3.2 application.yml关键配置项详解

yee: pay: merchant-no: 100000000000000 private-key-path: classpath:rsa_private_key.pem public-key-path: classpath:rsa_public_key.pem gateway-url: https://test.yeepay.com sandbox: true server: port: 8080 servlet: context-path: /epay-demo spring: servlet: encoding: charset: UTF-8 force: true main: allow-bean-definition-overriding: true

重点参数说明:

参数值说明
yee.pay.merchant-no100000000000000沙箱商户号,必须与沙箱后台一致,不能加引号(YAML中引号会导致类型为String而非Long,但易宝SDK内部强转为Long,引号反而引发NumberFormatException)
yee.pay.private-key-pathclasspath:rsa_private_key.pem私钥必须放在src/main/resources/下,且文件名严格匹配。源码中RSAUtil用getClass().getResourceAsStream()加载,路径错误会抛NullPointerException
server.servlet.context-path/epay-demo若设置此值,回调URL必须为http://your-domain/epay-demo/notify,需在易宝后台“异步通知地址”中精确填写

3.3 密钥文件格式校验:OpenSSL命令一行定生死

很多“测试失败”其实卡在密钥格式。易宝要求PKCS#8格式私钥,而OpenSSL默认生成PKCS#1。用以下命令校验并转换:

# 查看私钥格式(输出含"BEGIN PRIVATE KEY"为PKCS#8,含"BEGIN RSA PRIVATE KEY"为PKCS#1) openssl rsa -in rsa_private_key.pem -text -noout # 若为PKCS#1,转为PKCS#8(易宝要求格式) openssl pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt -out rsa_private_key_pkcs8.pem # 将新文件重命名为源码期望的文件名 mv rsa_private_key_pkcs8.pem rsa_private_key.pem

玄学经验:Windows下用记事本保存的.pem文件可能含BOM头,导致RSAUtil读取时首行乱码,签名失败。务必用VS Code或Notepad++以UTF-8无BOM格式保存。


4. 避坑:五个让开发者凌晨三点还在查日志的真实问题

4.1 现象:下单跳转后显示“系统繁忙,请稍后再试”

原因:易宝网关返回HTTP 500,但源码未捕获RestTemplate异常,前端只看到空白页。深层原因是gatewayUrl末尾多了斜杠(如https://test.yeepay.com/),易宝网关拒绝该URL。
解决:检查yee.pay.gateway-url配置,严格去掉末尾斜杠。源码中PayService.buildSignedParams()拼接URL时已自带/index.do,多一个/会导致//index.do。

4.2 现象:回调验签始终失败,RSAUtil.verify()返回false

原因:notifyMap解析时未按字典序排序就拼接验签串。易宝验签要求参数顺序与下单签名完全一致,而HashMap无序。
解决:parseNotifyBody()方法必须返回TreeMap(自动排序):

private TreeMap<String, String> parseNotifyBody(String body) { TreeMap<String, String> map = new TreeMap<>(); Arrays.stream(body.split("&")) .filter(s -> s.contains("=")) .forEach(pair -> { String[] kv = pair.split("=", 2); // 限制分割次数,防value含=号 if (kv.length == 2) { map.put(kv[0], URLDecoder.decode(kv[1], StandardCharsets.UTF_8)); } }); return map; }

4.3 现象:本地IDE运行正常,打包成jar后启动报NullPointerException(私钥加载失败)

原因:RSAUtil.loadPrivateKey()中FileInputStream无法读取jar包内资源。源码正确做法是getClass().getResourceAsStream(),但有人误改成new FileInputStream("rsa_private_key.pem")。
解决:确认RSAUtil.java中私钥加载代码为:

InputStream is = RSAUtil.class.getClassLoader().getResourceAsStream(privateKeyPath.replace("classpath:", "")); if (is == null) { throw new RuntimeException("私钥文件未找到: " + privateKeyPath); }

4.4 现象:支付成功后,回调返回success,但订单状态未更新

原因:NotifyController.handleNotify()中return "success"被Spring Boot的@ResponseBody拦截,转成JSON响应{"success":true},而易宝只认纯文本success。
解决:方法返回类型必须为String,且不能加@ResponseBody(@PostMapping默认有),同时确保Content-Type为text/plain:

@PostMapping(value = "/notify", consumes = MediaType.ALL_VALUE, produces = MediaType.TEXT_PLAIN_VALUE) public String handleNotify(HttpServletRequest request) { ... }

4.5 现象:沙箱支付页显示“订单金额格式错误”

原因:orderAmount字段传了100.00(带小数点),但易宝沙箱要求整数分单位(如100元传10000)。
解决:PayRequest.setOrderAmount()必须传Long类型分值:

// 前端传100.00元,后端转为分 request.setOrderAmount(Math.round(Double.parseDouble(frontAmount) * 100));

源码中PayRequest.orderAmount字段类型为Long,若前端JSON传字符串"100.00",Jackson会反序列化失败。必须前端传数字,或后端加@JsonDeserialize(using = StringToLongDeserializer.class)。


5. 实战调试技巧:三招定位90%的支付链路问题

5.1 日志埋点黄金位置:在签名前后打快照

源码已在关键节点添加DEBUG日志,但你需要知道在哪看、怎么看。打开logback-spring.xml,确保com.example.epay.util.RSAUtil日志级别为DEBUG:

<logger name="com.example.epay.util.RSAUtil" level="DEBUG"/>

然后关注这两处日志:

  • 下单签名前:RSAUtil.java第45行log.debug("待签名字符串: {}", signString);
    这是你提交给易宝的原始参数串,复制出来,用在线工具(如https://www.sojson.com/rsa/)手动验签,确认私钥是否正确。

  • 回调验签前:RSAUtil.java第82行log.debug("验签字符串: {}", verifyString);
    这是易宝回调参数拼接后的字符串,与下单签名串必须完全一致(除sign字段外)。若不一致,说明参数解析有误(如URL解码失败、空格未处理)。

血泪经验:某次发现verifyString比signString多一个\r\n,追查发现BufferedReader.readLine()丢弃了换行符,但notifyMap中value值末尾被意外拼入\r。解决方案是在parseNotifyBody()中对每个value调用.trim()。

5.2 沙箱环境抓包:用Chrome DevTools看真实请求

易宝沙箱页面是HTTPS,但你可以用Chrome的Network面板看跳转过程:

  1. 前端调用/create-order后,拿到redirectUrl;
  2. 在Chrome地址栏粘贴该URL,按F12打开DevTools;
  3. 切到Network标签,勾选“Preserve log”;
  4. 回车访问,观察index.do请求的Query参数;
  5. 重点核对:sign字段长度是否为344字符(SHA256withRSA Base64编码后固定长度),若为172字符则是MD5签名(配置错误)。

若sign为空,说明PayService.buildSignedParams()未执行到签名步骤,大概率是merchantNo为空或privateKeyPath加载失败。

5.3 回调模拟:用curl命令绕过易宝,直测你的Notify接口

不必等易宝回调,用curl模拟真实请求:

# 构造易宝回调Body(从日志中复制verifyString,再拼上sign) curl -X POST http://localhost:8080/epay-demo/notify \ -H "Content-Type: text/plain" \ -d "result=1&orderNo=ORD1712345678901&orderAmount=10000&currency=156&payType=1001&sign=MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwEAAKCAMIID8jCCA9qgAwIBAgIIUoLmOvQAAAAAMIIDvTCCA7WgAwIBAgIIVt1n4h4AAAACMA0GCSqGSIb3DQEBCwUAMIGLMQswCQYDVQQGEwJDTjELMAkGA1UECBMCUE4xCzAJBgNVBAcTAlBITDELMAkGA1UEChMCUE4xCzAJBgNVBAsTAlBITDELMAkGA1UEAxMCUE4xFTATBgNVBC0TDFBTUFJPTV9DQTEiMCAGCSqGSIb3DQEJARYTYWRtaW5AYWRtaW4uYWRtaW4uYWRtaW4wHhcNMjIwMTAxMDAwMDAwWhcNMzIwMTAxMDAwMDAwWjCBizELMAkGA1UEBhMCQ04xCzAJBgNVBAgTAlBOMQswCQYDVQQHEwJQTjELMAkGA1UEChMCUE4xCzAJBgNVBAsTAlBOMQswCQYDVQQDEwJQTjEVMBMGA1UELRMMUFNQUk9NX0NBMSIwIAYJKoZIhvcNAQkBFhNhZG1pbkBhZG1pbi5hZG1pbi5hZG1pbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL...[省略]" # 观察返回值是否为"success",并检查控制台日志

后悔药:如果curl返回fail,立刻检查日志中验签字符串是否与你构造的verifyString完全一致(包括空格、换行、大小写)。90%的验签失败源于字符串不一致,而非密钥错误。

从那以后我每次部署新环境,都强制走一遍这三步:① 用curl模拟回调打桩;② 把日志中的signString和verifyString复制到文本对比工具逐字符比对;③ 用OpenSSL命令行对signString重新签名,看Base64结果是否与日志中sign字段一致。这三步做完,支付链路的不确定性就从“玄学”变成了“可计算”。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询