Spring Boot项目实战:手把手教你集成银联B2B无卡支付(SM2国密证书版)
Spring Boot实战:银联B2B无卡支付集成全流程解析(SM2国密证书版)
在企业级应用开发中,支付功能是不可或缺的核心模块。银联B2B无卡支付作为国内企业间交易的重要渠道,其安全性和稳定性备受开发者关注。本文将带你从零开始,基于Spring Boot框架完整实现银联B2B无卡支付集成,重点解决SM2国密证书配置、签名验签等关键问题。
1. 环境准备与前置条件
1.1 开发资源获取
在开始编码前,需要从银联对接人员处获取以下材料:
- 开发文档:通常命名为
ChinaPay新一代商户接入手册_YYYYMMDD.pdf - 证书文件包:
- 公钥证书
CP.rar(包含.cer格式证书) - 私钥压缩包
usexxx.zip(内含.sm2私钥文件和密码文本)
- 公钥证书
- SDK组件:
chinapaysecure-sm-1.0.jar核心库 - 测试账号:需银联开通B2B支付测试权限
- IP白名单:配置服务器公网IP到银联测试环境
注意:生产环境证书需单独申请,测试证书有效期通常为3个月
1.2 项目基础配置
创建Spring Boot项目时,建议采用以下依赖组合:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.unionpay</groupId> <artifactId>chinapaysecure-sm</artifactId> <version>1.0</version> <scope>system</scope> <systemPath>${project.basedir}/lib/chinapaysecure-sm-1.0.jar</systemPath> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>2. 证书配置与核心工具类封装
2.1 证书文件管理
推荐将证书文件存放在resources/cert目录下,通过Spring的ResourceLoader动态加载:
@Configuration public class CertConfig { @Value("classpath:cert/xxx.sm2") private Resource privateKey; @Value("classpath:cert/xxx.cer") private Resource publicCert; @Bean public String privateKeyPath() throws IOException { return privateKey.getFile().getAbsolutePath(); } @Bean public String publicCertPath() throws IOException { return publicCert.getFile().getAbsolutePath(); } }2.2 SecssUtil的Spring Bean化
银联SDK的核心工具类需要正确初始化:
@Configuration @Slf4j public class SecssConfig { @Value("${unionpay.security.config-path}") private String configPath; @Bean public SecssUtil secssUtil() { SecssUtil util = new SecssUtil(); if (!util.init(configPath)) { log.error("银联证书初始化失败: {}-{}", util.getErrCode(), util.getErrMsg()); throw new IllegalStateException("银联证书初始化失败"); } return util; } }对应的application.yml配置:
unionpay: security: config-path: /path/to/security.propertiessecurity.properties示例内容:
# SM2私钥配置 secss.privateAlg=SM2 secss.privatePath=/absolute/path/to/xxx.sm2 secss.privatePwd=your_password # SM2公钥配置 secss.publicAlg=SM2 secss.publicPath=/absolute/path/to/xxx.cer3. 支付流程实现
3.1 支付请求构建
银联B2B支付需要构造特定格式的请求参数:
public class UnionPayService { @Autowired private SecssUtil secssUtil; private static final String VERSION = "1.0.0"; private static final String BUSI_TYPE = "0501"; public Map<String, String> buildPayRequest(PayRequest request) { TreeMap<String, Object> params = new TreeMap<>(); // 基础参数 params.put("Version", VERSION); params.put("MerId", request.getMerchantId()); params.put("MerOrderNo", request.getOrderNo()); params.put("TranDate", formatDate(request.getTradeDate())); params.put("TranTime", formatTime(request.getTradeTime())); // 金额处理(元转分) params.put("OrderAmt", request.getAmount().multiply(BigDecimal.valueOf(100)).longValue()); // 业务参数 params.put("BusiType", BUSI_TYPE); params.put("MerBgUrl", request.getNotifyUrl()); params.put("BankInstNo", request.getBankCode()); // 签名处理 secssUtil.sign(params); params.put("Signature", secssUtil.getSign()); return params.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, e -> String.valueOf(e.getValue()) )); } }3.2 前端支付跳转
构建支付表单自动提交到银联网关:
<form id="unionpay-form" action="https://gateway.test.unionpay.com/b2b" method="post"> <input type="hidden" name="Version" th:value="${payParams.Version}"> <input type="hidden" name="MerId" th:value="${payParams.MerId}"> <!-- 其他参数... --> </form> <script> document.getElementById('unionpay-form').submit(); </script>4. 回调处理与交易查询
4.1 异步通知处理
银联支付结果通过异步通知返回,需实现验签逻辑:
@RestController @RequestMapping("/payment/unionpay") public class UnionPayCallbackController { @Autowired private SecssUtil secssUtil; @PostMapping("/notify") public String handleNotify(HttpServletRequest request) { Map<String, String> params = getAllRequestParams(request); // 验签检查 if (!secssUtil.verify(params)) { log.warn("银联回调验签失败: {}-{}", secssUtil.getErrCode(), secssUtil.getErrMsg()); return "error|验签失败"; } // 处理业务逻辑 processPaymentResult(params); return "success"; } private Map<String, String> getAllRequestParams(HttpServletRequest request) { return request.getParameterMap().entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, e -> String.join(",", e.getValue()) )); } }4.2 交易状态查询
支付完成后建议主动查询确认状态:
public PaymentResult queryPayment(String merOrderNo, String tranDate) { TreeMap<String, Object> params = new TreeMap<>(); params.put("Version", VERSION); params.put("MerId", merchantId); params.put("MerOrderNo", merOrderNo); params.put("TranDate", tranDate); params.put("TranType", "0502"); // 查询交易类型 secssUtil.sign(params); params.put("Signature", secssUtil.getSign()); // 发送HTTP请求到银联查询接口 String response = restTemplate.postForObject( "https://query.test.unionpay.com/api", params, String.class ); return parseQueryResult(response); }5. 关键问题解决方案
5.1 SM2证书路径问题
开发与生产环境证书路径处理的推荐方案:
public String resolveCertPath(String resourcePath) { try { Resource resource = new ClassPathResource(resourcePath); return resource.getFile().getAbsolutePath(); } catch (IOException e) { log.error("证书文件加载失败", e); throw new RuntimeException("证书加载异常"); } }5.2 金额精度处理
避免金额计算时的精度问题:
public class AmountUtils { private static final BigDecimal HUNDRED = new BigDecimal("100"); public static long yuanToFen(BigDecimal yuan) { return yuan.multiply(HUNDRED).longValueExact(); } public static BigDecimal fenToYuan(long fen) { return new BigDecimal(fen).divide(HUNDRED, 2, RoundingMode.HALF_UP); } }5.3 回调参数验签异常
常见验签失败原因及排查方法:
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 验签返回false | 证书未正确加载 | 检查证书路径和密码 |
| 验签返回false | 参数顺序错误 | 确保验签前参数按字母排序 |
| 验签返回false | 特殊字符未处理 | 对回调参数进行URL解码 |
5.4 国密算法兼容问题
SM2证书与其他系统的交互要点:
- 加密数据格式:银联采用C1C2C3格式
- 摘要算法:使用SM3而非SHA系列
- 密钥长度:256位SM2密钥对
// SM2加密示例 public String sm2Encrypt(String plainText) { secssUtil.encryptData(plainText); return secssUtil.getEncValue(); }6. 生产环境部署建议
6.1 证书安全管理
生产环境证书处理的最佳实践:
- 使用绝对路径配置证书文件
- 证书文件设置600权限(仅应用用户可读)
- 私钥密码通过环境变量注入,而非配置文件
- 定期监控证书有效期(建议提前1个月续期)
6.2 性能优化方案
高并发场景下的优化策略:
SecssUtil实例管理:
- 避免频繁创建新实例
- 推荐使用ThreadLocal缓存
HTTP连接池配置:
spring: resttemplate: pool: max-total: 100 default-max-per-route: 20- 异步通知处理:
- 采用消息队列削峰
- 实现幂等性处理
6.3 监控与日志
关键监控指标建议:
- 签名成功率:反映证书状态
- 回调处理耗时:监控系统性能
- 交易状态分布:分析支付成功率
日志记录要点:
log.info("银联交易请求: {}", JsonUtils.toJson(params).replaceAll("(\\\"\\w+Pwd\\\":\\\")(.*?)(\\\")", "$1****$3"));7. 测试验证流程
7.1 测试用例设计
必备的测试场景清单:
正常支付流程测试
- 不同金额边界值(0.01元,最大限额)
- 不同银行机构码测试
异常场景测试
- 证书过期场景
- 网络中断恢复测试
- 重复通知处理
性能压力测试
- 连续100次查询请求
- 并发20笔支付请求
7.2 联调检查清单
与银联对接时的验证要点:
- [ ] 证书加载是否成功
- [ ] 基础参数是否齐全
- [ ] 签名生成是否正常
- [ ] 异步通知能否接收
- [ ] 交易查询结果一致
7.3 常见错误代码
快速问题定位参考表:
| 错误码 | 含义 | 处理建议 |
|---|---|---|
| 1001 | 验签失败 | 检查证书和参数顺序 |
| 2005 | 交易不存在 | 确认交易日期和订单号 |
| 3002 | 金额格式错误 | 确认元转分计算 |
实际项目中遇到最棘手的问题是SM2证书在不同环境下的加载问题。通过将证书文件放在项目外部目录,配合启动参数指定路径的方式,最终实现了开发、测试、生产环境的统一配置方案。另一个经验是,银联的异步通知可能会有1-2秒的延迟,业务处理时需要做好并发控制。
