在 Spring Boot 中对接银联支付(以「银联在线支付」为例)的核心流程包括 商户入驻、参数配置、接口开发(下单/查询/回调)、签名验签、沙箱测试 等步骤。以下是详细实现流程:
一、前置准备
1. 注册银联商户
-
访问 银联开放平台,注册企业账号并完成实名认证。
-
登录后进入「产品中心」→「选择支付产品」(如「互联网支付」),提交申请(需提供营业执照、银行账户等信息)。
-
审核通过后,在「商户中心」获取关键参数:
- 商户号(merId):商户唯一标识(如
777290058110097)。 - API 密钥(key):用于生成签名的私钥(需在商户中心生成,长度通常为 1024 或 2048 位)。
- 证书文件:银联提供的公钥证书(
acp_test_sign.pfx或生产环境的.pfx),用于验签。 - 沙箱/生产环境 URL:沙箱环境用于测试(如
https://101.231.204.84:5000),生产环境为https://gateway.95516.com。
- 商户号(merId):商户唯一标识(如
2. 环境与依赖
-
Spring Boot 版本:建议 2.7.x 及以上(兼容 Java 8+)。
-
添加 Maven 依赖(根据需求选择):
<!-- HTTP 客户端(OkHttp) --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.12.0</version> </dependency> <!-- 加密库(Bouncy Castle,用于处理 RSA 签名) --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency> <!-- JSON 处理 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- 工具类(可选) --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.20</version> </dependency>
二、核心配置
在 application.yml 中配置银联相关参数(敏感信息建议通过环境变量或配置中心管理):
unionpay:
mer-id: 777290058110097 # 商户号
api-key: your_api_key # API 密钥(私钥)
cert-path: classpath:acp_test_sign.pfx # 银联公钥证书路径(沙箱)
cert-pwd: 123456 # 证书密码(默认是商户号)
env: test # 环境(test/prod)
# 沙箱/生产环境 URL
gateways:
test: https://101.231.204.84:5000/gateway/api
prod: https://gateway.95516.com/gateway/api
三、核心功能开发
1. 签名工具类(关键)
银联支付要求对请求参数进行 RSA 签名,签名规则需严格按照银联文档执行。以下是签名工具类的示例:
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.StrUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
public class UnionPaySigner {
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* 生成 RSA 签名
* @param params 待签名参数(键值对,需按字典序排序)
* @param privateKey 私钥(PKCS#8 格式)
* @return Base64 编码的签名值
*/
public static String sign(Map<String, String> params, String privateKey) throws Exception {
// 1. 过滤空值并按字典序排序参数
List<String> sortedKeys = new ArrayList<>(params.keySet());
Collections.sort(sortedKeys);
StringBuilder sb = new StringBuilder();
for (String key : sortedKeys) {
String value = params.get(key);
if (StrUtil.isNotBlank(value)) {
sb.append(key).append("=").append(value).append("&");
}
}
String signData = sb.substring(0, sb.length() - 1); // 去掉末尾的 &
// 2. 加载私钥(PKCS#8 格式)
byte[] keyBytes = Base64.decode(privateKey);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
PrivateKey priKey = keyFactory.generatePrivate(spec);
// 3. RSA 签名(SHA256 算法)
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, priKey);
byte[] signedBytes = cipher.doFinal(signData.getBytes("UTF-8"));
// 4. Base64 编码签名结果
return Base64.encode(signedBytes);
}
}
2. 支付下单接口
调用银联「统一收单线下交易预创建」接口(示例接口,具体以银联文档为准),生成支付链接或二维码。
请求参数示例(部分必填参数):
Map<String, String> reqParams = new HashMap<>();
reqParams.put("version", "5.1.0"); // 版本号(固定)
reqParams.put("encoding", "UTF-8"); // 编码
reqParams.put("txnType", "01"); // 交易类型(01:消费)
reqParams.put("txnSubType", "01"); // 交易子类(01:自助消费)
reqParams.put("bizType", "000000"); // 业务类型(通用)
reqParams.put("frontUrl", "http://xxx.com/success"); // 前台跳转地址(支付成功回调)
reqParams.put("backUrl", "http://xxx.com/notify"); // 后台通知地址(异步回调)
reqParams.put("signMethod", "01"); // 签名方式(01:RSA)
reqParams.put("channelType", "07"); // 渠道类型(07:互联网)
reqParams.put("accessType", "0"); // 接入类型(0:直连商户)
reqParams.put("merId", unionPayConfig.getMerId()); // 商户号
reqParams.put("orderId", "ORDER_" + System.currentTimeMillis()); // 商户订单号(唯一)
reqParams.put("txnTime", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())); // 订单时间
reqParams.put("txnAmt", "100"); // 交易金额(分,如 100 元=10000 分?需确认银联要求)
reqParams.put("currencyCode", "156"); // 货币代码(156:人民币)
发送请求并签名:
public String createOrder(Map<String, String> reqParams) throws Exception {
// 1. 生成签名
String privateKey = unionPayConfig.getApiKey(); // 从配置获取私钥
String sign = UnionPaySigner.sign(reqParams, privateKey);
reqParams.put("signature", sign); // 添加签名到请求参数
// 2. 构造 XML 请求体(银联接口通常使用 XML 格式)
String xml = mapToXml(reqParams); // 需要实现 map 转 XML 的工具方法
// 3. 发送 POST 请求到银联网关
OkHttpClient client = new OkHttpClient();
RequestBody body = RequestBody.create(xml, MediaType.get("application/xml"));
Request request = new Request.Builder()
.url(getGatewayUrl()) // 根据环境选择沙箱/生产 URL
.post(body)
.build();
Response response = client.newCall(request).execute();
String respXml = response.body().string();
// 4. 解析响应(返回 XML,包含交易流水号 txnId、支付链接等)
Map<String, String> respMap = xmlToMap(respXml); // 实现 XML 转 map 的工具方法
if ("00".equals(respMap.get("respCode"))) {
return respMap.get("htmlUrl"); // 返回前端跳转的支付页面 URL
} else {
throw new RuntimeException("下单失败:" + respMap.get("respMsg"));
}
}
3. 异步回调处理(关键)
银联支付完成后,会通过 backUrl(前台跳转)和 notifyUrl(后台异步通知)返回结果。必须以异步通知(notifyUrl)的验签结果为准。
处理步骤:
- 接收银联 POST 请求的 XML 数据。
- 验证签名(确保数据来自银联)。
- 解析业务参数(如订单状态
respCode、交易金额txnAmt)。 - 更新本地订单状态(如支付成功则标记为已支付)。
- 返回
success给银联(否则银联会重复通知)。
代码示例:
@PostMapping("/notify")
public String handleNotify(HttpServletRequest request) throws Exception {
// 1. 读取请求体(XML 数据)
BufferedReader reader = request.getReader();
StringBuilder xmlSb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
xmlSb.append(line);
}
String reqXml = xmlSb.toString();
// 2. 解析 XML 到 Map
Map<String, String> reqParams = xmlToMap(reqXml);
// 3. 验证签名
boolean isValid = verifySign(reqParams);
if (!isValid) {
log.error("银联回调签名验证失败");
return "fail";
}
// 4. 检查业务状态(respCode=00 表示成功)
String respCode = reqParams.get("respCode");
String orderId = reqParams.get("orderId"); // 商户订单号
String txnAmt = reqParams.get("txnAmt"); // 交易金额(分)
if ("00".equals(respCode)) {
// 更新订单状态为已支付(根据 orderId 查询本地订单并修改)
orderService.updateOrderStatus(orderId, "PAID");
} else {
log.error("支付失败,订单号:{},原因:{}", orderId, reqParams.get("respMsg"));
}
// 5. 返回 success 告知银联
return "success";
}
/**
* 验证银联回调签名
*/
private boolean verifySign(Map<String, String> params) throws Exception {
// 1. 提取银联公钥(从证书文件加载)
X509Certificate acpCert = loadAcpCertificate(); // 实现加载证书的方法
PublicKey publicKey = acpCert.getPublicKey();
// 2. 分离签名值和业务参数(去掉 signature 字段)
String sign = params.remove("signature");
if (StrUtil.isBlank(sign)) {
return false;
}
// 3. 按字典序排序参数并拼接
List<String> sortedKeys = new ArrayList<>(params.keySet());
Collections.sort(sortedKeys);
StringBuilder sb = new StringBuilder();
for (String key : sortedKeys) {
String value = params.get(key);
if (StrUtil.isNotBlank(value)) {
sb.append(key).append("=").append(value).append("&");
}
}
String signData = sb.substring(0, sb.length() - 1);
// 4. RSA 验签(SHA256 算法)
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] decryptedBytes = cipher.doFinal(Base64.decode(sign));
// 5. 比较解密后的签名原文与拼接的 signData
String originalSign = new String(decryptedBytes, "UTF-8");
return originalSign.equals(signData);
}
/**
* 加载银联公钥证书(从 resources 或文件系统)
*/
private X509Certificate loadAcpCertificate() throws Exception {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
InputStream is = getClass().getResourceAsStream("/acp_test_sign.cer"); // 证书路径
return (X509Certificate) factory.generateCertificate(is);
}
四、沙箱测试
-
测试工具:使用银联提供的 沙箱测试平台 模拟支付。
-
测试场景:
- 正常支付(输入测试卡号:
622202******1234,有效期任意,CVN2 任意)。 - 支付失败(如金额超限、卡片冻结)。
- 重复回调(验证系统是否幂等)。
- 正常支付(输入测试卡号:
-
常见问题排查:
- 签名错误:检查签名算法(RSA/SHA256)、参数排序、证书是否正确。
- 异步通知未收到:检查
backUrl是否可公网访问(银联服务器需能调用)、防火墙是否拦截。 - 订单状态未更新:确认
orderId唯一性,检查数据库事务是否提交。
五、生产环境上线
- 替换配置:将
env改为prod,cert-path指向生产环境的.pfx证书(需在银联商户中心下载)。 - 关闭沙箱测试:确保所有接口调用指向生产环境 URL(
https://gateway.95516.com)。 - 监控与日志:添加支付接口的日志记录(请求/响应参数、耗时),监控异常订单。
六、注意事项
- 证书安全:私钥(
.pfx)和 API 密钥需严格保密,禁止硬编码在代码中(建议通过配置中心或环境变量注入)。 - 幂等性:异步通知可能重复发送,需通过
orderId或txnId做幂等校验,避免重复更新订单。 - 合规性:遵守银联的交易规则(如退款时效、交易限额),保留交易日志至少 1 年。
- 文档更新:银联接口可能调整,需定期查看 银联开放平台文档 获取最新规范。
通过以上步骤,可实现 Spring Boot 与银联支付的对接。实际开发中需结合银联最新接口文档(版本可能升级,如 5.1.0 可能更新为 5.2.0)调整参数和逻辑。