springboot对接银联支付的详细步骤流程

243 阅读7分钟

在 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

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)的验签结果为准​​。

​处理步骤​​:

  1. 接收银联 POST 请求的 XML 数据。
  2. 验证签名(确保数据来自银联)。
  3. 解析业务参数(如订单状态 respCode、交易金额 txnAmt)。
  4. 更新本地订单状态(如支付成功则标记为已支付)。
  5. 返回 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);
}

​四、沙箱测试​

  1. ​测试工具​​:使用银联提供的 沙箱测试平台 模拟支付。

  2. ​测试场景​​:

    • 正常支付(输入测试卡号:622202******1234,有效期任意,CVN2 任意)。
    • 支付失败(如金额超限、卡片冻结)。
    • 重复回调(验证系统是否幂等)。
  3. ​常见问题排查​​:

    • ​签名错误​​:检查签名算法(RSA/SHA256)、参数排序、证书是否正确。
    • ​异步通知未收到​​:检查 backUrl 是否可公网访问(银联服务器需能调用)、防火墙是否拦截。
    • ​订单状态未更新​​:确认 orderId 唯一性,检查数据库事务是否提交。

​五、生产环境上线​

  1. 替换配置:将 env 改为 prodcert-path 指向生产环境的 .pfx 证书(需在银联商户中心下载)。
  2. 关闭沙箱测试:确保所有接口调用指向生产环境 URL(https://gateway.95516.com)。
  3. 监控与日志:添加支付接口的日志记录(请求/响应参数、耗时),监控异常订单。

​六、注意事项​

  • ​证书安全​​:私钥(.pfx)和 API 密钥需严格保密,禁止硬编码在代码中(建议通过配置中心或环境变量注入)。
  • ​幂等性​​:异步通知可能重复发送,需通过 orderIdtxnId 做幂等校验,避免重复更新订单。
  • ​合规性​​:遵守银联的交易规则(如退款时效、交易限额),保留交易日志至少 1 年。
  • ​文档更新​​:银联接口可能调整,需定期查看 银联开放平台文档 获取最新规范。

通过以上步骤,可实现 Spring Boot 与银联支付的对接。实际开发中需结合银联最新接口文档(版本可能升级,如 5.1.0 可能更新为 5.2.0)调整参数和逻辑。