Skip to content

RSA 签名机制

概述

米付科技聚合支付平台采用 RSA 非对称加密 签名机制,确保接口请求的安全性和完整性。所有接口请求都需要进行签名验证。

签名算法

  • 签名算法: RSA-SHA256
  • 编码方式: Base64
  • 密钥长度: 2048 位

签名流程

1. 参数排序

将所有请求参数(除 sign 字段外)按照参数名 ASCII 码从小到大排序:

javascript
// 原始参数
{
  "pay_mch_id": "1234567890",
  "out_trade_no": "20240101120000",
  "total_amount": 100,
  "timestamp": "1704067200",
  "nonce_str": "abc123def456"
}

// 排序后
[
  "pay_mch_id=1234567890",
  "nonce_str=abc123def456",
  "out_trade_no=20240101120000",
  "timestamp=1704067200",
  "total_amount=100"
]

2. 拼接字符串

将排序后的参数用 & 拼接:

pay_mch_id=1234567890&nonce_str=abc123def456&out_trade_no=20240101120000&timestamp=1704067200&total_amount=100

3. 生成签名

使用商户私钥对拼接后的字符串进行 RSA-SHA256 签名,然后进行 Base64 编码:

javascript
sign = Base64(RSA-SHA256(signString, privateKey))

4. 添加签名到请求

将签名值添加到请求参数中:

json
{
  "pay_mch_id": "1234567890",
  "out_trade_no": "20240101120000",
  "total_amount": 100,
  "timestamp": "1704067200",
  "nonce_str": "abc123def456",
  "sign_type": "RSA",
  "sign": "签名值"
}

代码示例

Java 示例

java
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;

public class SignUtil {
    
    /**
     * 生成 RSA 签名
     */
    public static String sign(Map<String, Object> params, String privateKey) throws Exception {
        // 1. 参数排序(使用 TreeMap 自动排序)
        TreeMap<String, Object> sortedParams = new TreeMap<>(params);
        
        // 2. 拼接字符串
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, Object> entry : sortedParams.entrySet()) {
            if (entry.getValue() != null && !entry.getKey().equals("sign")) {
                sb.append(entry.getKey())
                  .append("=")
                  .append(entry.getValue())
                  .append("&");
            }
        }
        // 移除最后一个 &
        String signString = sb.substring(0, sb.length() - 1);
        
        // 3. 生成签名
        byte[] keyBytes = Base64.getDecoder().decode(privateKey);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey key = keyFactory.generatePrivate(keySpec);
        
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(key);
        signature.update(signString.getBytes("UTF-8"));
        
        return Base64.getEncoder().encodeToString(signature.sign());
    }
    
    /**
     * 验证签名
     */
    public static boolean verify(Map<String, Object> params, String publicKey, String sign) throws Exception {
        // 1. 参数排序
        TreeMap<String, Object> sortedParams = new TreeMap<>(params);
        sortedParams.remove("sign");
        
        // 2. 拼接字符串
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, Object> entry : sortedParams.entrySet()) {
            if (entry.getValue() != null) {
                sb.append(entry.getKey())
                  .append("=")
                  .append(entry.getValue())
                  .append("&");
            }
        }
        String signString = sb.substring(0, sb.length() - 1);
        
        // 3. 验证签名
        byte[] keyBytes = Base64.getDecoder().decode(publicKey);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey key = keyFactory.generatePublic(new java.security.spec.X509EncodedKeySpec(keyBytes));
        
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initVerify(key);
        signature.update(signString.getBytes("UTF-8"));
        
        return signature.verify(Base64.getDecoder().decode(sign));
    }
}

Node.js 示例

javascript
const crypto = require('crypto');

/**
 * 生成 RSA 签名
 */
function sign(params, privateKey) {
  // 1. 参数排序
  const sortedKeys = Object.keys(params)
    .filter(key => key !== 'sign' && params[key] !== null && params[key] !== undefined)
    .sort();
  
  // 2. 拼接字符串
  const signString = sortedKeys
    .map(key => `${key}=${params[key]}`)
    .join('&');
  
  // 3. 生成签名
  const sign = crypto
    .createSign('RSA-SHA256')
    .update(signString)
    .sign(privateKey, 'base64');
  
  return sign;
}

/**
 * 验证签名
 */
function verify(params, publicKey, sign) {
  // 1. 参数排序
  const sortedKeys = Object.keys(params)
    .filter(key => key !== 'sign' && params[key] !== null && params[key] !== undefined)
    .sort();
  
  // 2. 拼接字符串
  const signString = sortedKeys
    .map(key => `${key}=${params[key]}`)
    .join('&');
  
  // 3. 验证签名
  return crypto
    .createVerify('RSA-SHA256')
    .update(signString)
    .verify(publicKey, sign, 'base64');
}

// 使用示例
const params = {
  pay_mch_id: '1234567890',
  out_trade_no: '20240101120000',
  total_amount: 100,
  timestamp: '1704067200',
  nonce_str: 'abc123def456'
};

const privateKey = `-----BEGIN PRIVATE KEY-----
Your Private Key Here
-----END PRIVATE KEY-----`;

const signValue = sign(params, privateKey);
console.log('签名值:', signValue);

PHP 示例

php
<?php

class SignUtil {
    
    /**
     * 生成 RSA 签名
     */
    public static function sign($params, $privateKey) {
        // 1. 参数排序
        ksort($params);
        
        // 2. 拼接字符串
        $signString = '';
        foreach ($params as $key => $value) {
            if ($key !== 'sign' && $value !== null && $value !== '') {
                $signString .= $key . '=' . $value . '&';
            }
        }
        $signString = rtrim($signString, '&');
        
        // 3. 生成签名
        $privateKey = openssl_pkey_get_private($privateKey);
        openssl_sign($signString, $sign, $privateKey, OPENSSL_ALGO_SHA256);
        
        return base64_encode($sign);
    }
    
    /**
     * 验证签名
     */
    public static function verify($params, $publicKey, $sign) {
        // 1. 参数排序
        ksort($params);
        unset($params['sign']);
        
        // 2. 拼接字符串
        $signString = '';
        foreach ($params as $key => $value) {
            if ($value !== null && $value !== '') {
                $signString .= $key . '=' . $value . '&';
            }
        }
        $signString = rtrim($signString, '&');
        
        // 3. 验证签名
        $publicKey = openssl_pkey_get_public($publicKey);
        $result = openssl_verify($signString, base64_decode($sign), $publicKey, OPENSSL_ALGO_SHA256);
        
        return $result === 1;
    }
}

// 使用示例
$params = [
    'pay_mch_id' => '1234567890',
    'out_trade_no' => '20240101120000',
    'total_amount' => 100,
    'timestamp' => '1704067200',
    'nonce_str' => 'abc123def456'
];

$privateKey = file_get_contents('private_key.pem');
$signValue = SignUtil::sign($params, $privateKey);
echo "签名值: " . $signValue . PHP_EOL;

密钥管理

生成密钥对

使用 OpenSSL 生成

bash
# 生成私钥(2044位)
openssl genrsa -out private_key.pem 2048

# 生成公钥
openssl rsa -in private_key.pem -pubout -out public_key.pem

密钥格式

私钥格式(PKCS#8):

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
...
-----END PRIVATE KEY-----

公钥格式:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
...
-----END PUBLIC KEY-----

密钥上传

  1. 登录米付科技商户平台
  2. 进入「密钥管理」页面
  3. 上传您的公钥文件
  4. 平台将返回平台公钥(用于验签)

注意事项

  1. 私钥保密: 商户私钥必须严格保密,不可泄露
  2. 公钥上传: 商户公钥需上传至平台,用于平台验证商户签名
  3. 平台公钥: 平台公钥用于商户验证平台返回数据的签名
  4. 定期更换: 建议定期更换密钥对,提高安全性
  5. 密钥长度: 必须使用 2048 位或以上密钥长度

验签流程

接收平台返回的响应时,需要验证平台签名:

  1. 提取响应数据中的 sign 字段
  2. 将其余参数按 ASCII 排序并拼接
  3. 使用平台公钥验证签名
  4. 验证通过后才处理业务数据

常见问题

Q: 签名验证失败怎么办?

  1. 检查参数排序是否正确
  2. 检查拼接字符串格式是否正确
  3. 确认使用的私钥/公钥是否匹配
  4. 检查字符编码是否统一为 UTF-8
  5. 确认空值参数是否已排除

Q: 是否支持其他签名算法?

当前仅支持 RSA-SHA256 签名算法,暂不支持 MD5、HMAC 等其他算法。

Q: 时间戳有效期是多久?

请求时间戳与服务器时间相差超过 5 分钟 的请求将被拒绝。

Copyright © 2024 米付科技